Обзор
возможностей библиотеки OpenGL
Читатель, наверное,
знает, что OpenGL это оптимизированная, высокопроизводительная графическая библиотека
функций и типов данных для отображения двух-и трехмерной графики. Стандарт OpenGL
был утвержден в 1992 г. Он основан на библиотеке IRIS GL, разработанной компанией
Silicon Graphics (www.sgi.com). OpenGL поддерживают все платформы. Кроме того,
OpenGL поддержана аппа-ратно. Существуют видеокарты с акселераторами и специализированные
SD-кар-ты, которые выполняют примитивы OpenGL на аппаратном уровне.
Материал первой
части этого урока навеян очень хорошей книгой (доступной в online-варианте)
издательства Addison-Wesley «OpenGL Programming Guide, The Official Guide to
Learning OpenGL». Если читатель владеет английским языком, то мы рекомендуем
ее прочесть.
Подключаемые
библиотеки
Microsoft-реализация
OpenGL включает полный набор команд OpenGL, то есть глобальных функций, входящих
в ядро библиотеки OPENGL32.LIB и имеющих префикс gl (например, glLineWidth).
Заметьте, что функции из ядра библиотеки имеют множество версий, что позволяет
задать желаемый параметр или настройку любым удобным вам способом. Посмотрите
справку по функциям из семейства glColor*. Оказывается, что задать текущий цвет
можно 32 способами. Например, функция:
void
glColorSb(GLbyte red, GLbyte green, GLbyte blue);
определяет
цвет тремя компонентами типа GLbyte, а функция
void
glColor4dv(const GLdouble *v) ;
задает его
с помощью адреса массива из четырех компонентов.
С учетом этих
вариантов ядро библиотеки содержит более 300 команд. Кроме того, вы можете подключить
библиотеку утилит GLU32.LIB, которые дополняют основное ядро. Здесь есть функции
управления текстурами, преобразованием координат, генерацией сфер, цилиндров
и дисков, сплайновых аппроксимаций кривых и поверхностей (NURBS — Non-Uniform
Rational B-Spline), а также обработки ошибок. Еще одна, дополнительная (auxiliary)
библиотека GLAUX.LIB позволяет простым способом создавать Windows-окна, изображать
некоторые SD-объекты, обрабатывать события ввода и управлять фоновым процессом.
К сожалению, эта библиотека не документирована. Компания Microsoft не рекомендует
пользоваться ею для разработки коммерческих проектов, так как она содержит код
цикла обработки сообщений, в который невозможно вставить обработку других произвольных
сообщений.
Примечание
Тип GLbyte эквивалентен
типу signed char, a GLdouble — типу double. Свои собственные типы используются
в целях упрощения переносимости на другие платформы. Список типов OpenGL мы
приведем ниже. Четвертый компонент цвета определяет прозрачность цвета, то
есть способ смешивания цвета фона с цветом изображения. Некоторые команды
OpenGL имеют в конце символ v, который указывает, что ее аргументом должен
быть адрес массива (вектора). Вектор в математике — это последовательность
чисел (координат), единственным образом задающих элемент векторного пространства.
Многие команды имеют несколько версий, позволяя в конечном счете задать вектор
разными способами.
Около двадцати
Windows GDI-функций создано специально для работы с OpenGL. Большая часть из
них имеет префикс wgl (аббревиатура от Windows GL). Эти функции являются аналогами
функций с префиксом glx, которые подключают OpenGL к платформе X window System.
Наконец, существует несколько Win32-функций для управления форматом пикселов
и двойной буферизацией. Они применимы только для специализированных окон OpenGL.
|
Ограничения
Microsoft
К сожалению,
Microsoft-реализация OpenGL имеет ряд ограничений, которые не дают в полной
мере использовать всю мощь библиотек. Перечислим те из них, которые приведены
в документации MSDN.
Нет поддержки более
новой и популярной библиотеки GLUT, которая в некотором роде аналогична
библиотеке GLAUX. Эта проблема решается с помощью Интернет. Вы достаете glut32.dll,
glut32.tib и glut.h, которые затем помещаете в следующие папки: WINNT\System32,...\VC7\Lib\H...VC7\Inctude\GL
После этого следует указать компоновщику на необходимость подключения glut32.lib.
Это делается вместе с подключением других двух библиотек opengl32.lib и glu32.lib
(см. ниже).
Изображение OpenGL можно
вывести на печать только с помощью метафайлов (списка рисующих команд GDI).
При этом надо учитывать специфику, описанную в документации.
Нет поддержки стереоскопических
изображений.
OpenGL и GDI-графику
можно совмещать только в окне с одинарной буферизацией.
Windows имеет одну системную
цветовую палитру, которая применяется ко всему экрану, поэтому окно OpenGL
не может иметь собственной аппаратной палитры, но может иметь собственную
логическую палитру.
Окно OpenGL не поддерживает
динамический обмен данными (DDE), обмен с помощью механизма Clipboard и OLE.
Однако существуют обходные пути для использования операций с Clipboard
Библиотеки классов, такие
как Volumizer и Open Inventor, которые обеспечивают более высокий уровень
конструирования 3-D графики, не включены в состав Microsoft-реализации OpenGL.
Это, на мой взгляд, является очень серьезным недостатком.
|
Примитивы
OpenGL
Моделью или
объектом в OpenGL называется структура в памяти, конструируемая из геометрических
примитивов: точек, линий и полигонов, которые, в свою очередь, задаются своими
вершинами (vertices). Из этих моделей OpenGL создает изображение в специально
подготовленном окне. Процесс создания и демонстрации изображения называется
передачей (rendering) изображения OpenGL. Конечным изображением является множество
пикселов — мельчайших видимых элементов экранной поверхности. Информация о цвете
пикселов размещена в памяти в виде битовых плоскостей (bitplanes). Так называется
область памяти, которая содержит только один бит информации обо всех пикселах
окна. В совокупности плоскости составляют буфер кадра (framebuffer), который
содержит информацию, необходимую для того, чтобы дисплей отобразил все пикселы
окна OpenGL.
OpenGL изображает
графические примитивы (точки, сегменты линий или многоугольники), используя
при этом множество независимо управляемых режимов (modes). Для задания примитивов,
установки режимов и выполнения других операций необходимо вызывать функции OpenGL,
или, как принято говорить, давать последовательность команд OpenGL Примитивы
задаются своими вершинами, то есть точками трехмерного пространства. Кроме координат
с каждой вершиной ассоциируются такие данные, как цвет, направление нормали
(перпендикуляра), параметры текстуры и флаги границы (edge flags). Текстурами
называются готовые bitmap-изображения, которые накладываются на многоугольники
каркаса модели и вносят в нее эффект поверхности реального материала.
|
OpenGL
— автомат с конечным числом состояний
OpenGL работает
по принципу конечного автомата, то есть автомата, который в каждый момент времени
находится в одном из состояний, принадлежащих конечному множеству допустимых
значений. В документации вы можете встретить в применении к OpenGL термины state
machine (конечный автомат) и assembly line (конвейер). Некоторые команды (вызовы
функций OpenGL) переводят автомат в различные состояния или режимы, которые
остаются неизменными до тех пор, пока не придет следующая команда изменения
состояния. Текущий цвет, как вы видели, является одним из состояний. Другими
состояниями являются:
узор линий или полигонов
(stipple patterns);
тип проективных или обзорных
преобразований (projection and viewing transformations);
режимы рисования полигонов;
режимы упаковки пикселов;
расположение источников
света и свойства материалов.
Многие переменные,
определяющие состояния, переключаются с помощью функций glEnable (включить)
или gioisable (выключить). Каждая переменная состояния или режим имеет значение
по умолчанию, и в любой точке программы вы можете узнать текущее состояние.
Обычно для этой цели используется одна из 6-ТИ команд: glGetBooleanv, glGetDoublev,
glGetFloatv, glGetlntegerv, glGetPointerv или glisEnabled. Выбор зависит от
типа данных, которые задают состояние. Некоторые переменные состояния (state
variables) заполняются более специфичными командами, например: glGetLight*,
glGetError, glGetPolygonStipple. Множество состояний можно сохранить в стеке
атрибутов командами glPushAttrib или glPushClientAttrib. Обычно так делают для
того, чтобы временно изменить что-то, а затем восстановить состояния с помощью
одной из команд: glPopAttrib, glPopClientAttrib.
|
Конвейер
передачи OpenGL
Команды OpenGL
претерпевают одинаковый порядок обработки, проходя через последовательность
стадий, называемых конвейером обработки OpenGL (processing or rendering pipeline).
Схема конвейера приводится во многих источниках, приведем ее и мы (рис. 6.1),
для того чтобы не отсылать читателя к другим книгам. Ниже следует краткое описание
его основных блоков.
Рис.
6.1. Схема конвейера OpenGL
Списки
команд OpenGL (Display Lists)
Все данные,
описывающие геометрию или отдельные пикселы, могут быть сохранены в списках
команд (display lists) для последующего использования. Альтернатива — немедленное
использование (immediate mode). При вызове списка командой glCallList сохраненные
данные из списка начинают двигаться по конвейеру так же, как и в режиме немедленного
использования.
Вычислители
(Evaluators)
Все геометрические
примитивы описываются своими вершинами. Параметрические кривые и поверхности
могут изначально-быть описаны контрольными точками или базовыми функциями (обычно
полиномиальными). Вычислители — это методы, которые генерируют координаты вершин,
нормали к поверхности, координаты текстур и цвета точек, опираясь на контрольные
точки.
Сборка
примитивов
На этом этапе
происходит преобразование вершин в примитивы. Пространственные координаты (х,
у, z) преобразовываются с помощью матриц размерностью (4 х 4). Основная цель
— получить экранные, двухмерные координаты из трехмерных, мировых координат.
Если включен режим генерации текстуры, то она создается на этом этапе. Освещенность
вычисляется исходя из координат вектора нормали, расположения источников света,
отражающих свойств материала, углов конусов света и параметров его аттенюации
(ослабления). В результате получается цвет пиксела. Важным моментом на этапе
сборки примитивов (primitive assembly) является отсечение (clipping), то есть
удаление тех частей геометрии, которые попадают в невидимую часть пространства.
Точечное отсечение пропускает или не пропускает вершину. Отсечение линий или
полигонов подразумевает не только удаление вершин, но и возможное добавление
некоторых (промежуточных) вершин. На этом этапе происходит учет перспективы,
то есть уменьшение тех деталей сцены, которые расположены дальше от точки наблюдения,
и увеличение тех деталей, которые расположены ближе. Здесь используется понятие
видимого объема (viewport). Режим заполнения промежуточных точек полигона тоже
играет роль на этапе сборки.
Операции
с пикселами (Pixel Operations)
Данные о пикселах
следуют в конвейере OpenGL параллельным путем. Данные, хранимые в массивах системной
памяти, распаковываются с учетом набора возможных форматов, затем масштабируются,
сдвигаются и обрабатываются так называемой картой пикселов (pixel map). Результат
записывается либо в память текстуры, либо посылается на следующий этап — растеризацию.
Отметьте, что возможна обратная операция считывания пикселов. При этом также
Выполняются операции: масштабирование, сдвиг, преобразование и упаковка и помещение
в системную память. Существуют специальные операции копирования данных из буфера
кадра (framebuffer) в другую его часть или в буфер текстуры.
Сборка
текстуры (Texture Assembly)
Текстуры —
это bitmap-изображения, накладываемые на поверхности геометрических объектов
для придания эффекта фактуры реального материала. Текстурные объекты создаются
в OpenGL для упрощения их повторного использования. Использование текстур сопряжено
с большими затратами, поэтому в работе с ними применяют специальные ресурсы,
такие как texture memory. Так называют быструю видеопамять, приоритет использования
которой отдается текстурным объектам.
Растеризация
Так называют
преобразование как геометрических, так и данных о пикселах во фрагменты. Каждый
фрагмент соответствует пикселу в буфере кадра. При вычислении цвета фрагмента
учитывается большое количество факторов: узор штриховки полигона или линии,
толщина линии и размер точки, сглаживание зубчатости линий, тень объекта, режим
заполнения полигона, учет глубины изображения (факт видимости или невидимости)
и др.
Операции
с фрагментами
Каждая точка
уже двухмерного изображения характеризуется цветом, глубиной (значением координаты
Z) и данными о текстуре. Такая точка вместе с сопутствующей информацией называется
фрагментом. Фрагмент изменяет соответствующий ему пиксел в буфере кадра, если
он проходит пять тестов:
Pixel ownership-тест,
который проверяет принадлежность контексту, то есть не закрыт ли фрагмент
другим окном;
Scissor-тест, который
проверяет принадлежность вырезаемому прямоугольнику, который задается функцией
glScissor;
Alpha-тест, который проверяет
четвертый компонент цвета — прозрачность фрагмента с помощью функции glAlphaFunc;
Stencil-тест, используемый
при создании специальных эффектов. Он, например, проверяет, не попал ли фрагмент
в промежуток регулярного узора;
Depth-buffer-тест, который
проверяет, не закрыт ли фрагмент другим фрагментом с меньшей координатой Z.
Кроме того,
фрагмент претерпевает другие изменения.
текстурирование — это
генерация текстурного элемента (texel) на основе texture memory;
вычисление дымки (fog);
смешивание (blending);
интерполяция цвета (dithering);
логические операции;
маскирование с помощью
трафарета (bitmask).
|
Основные
этапы
Для того чтобы
запомнить основные этапы обработки, повторим ключевые моменты.
Основная линия конвейера
осуществляет преобразование по схеме: Вершины > Примитивы * Фрагменты
> Пикселы.
Параллельная линия обработки
исходных данных задает непосредственно пикселы.
Примитивы, заданные
в трехмерном пространстве, преобразуются в двухмерное изображение с помощью
растеризации.
Каждая точка уже двухмерного
изображения характеризуется цветом, глубиной (значением координаты Z) и данными
о текстуре. Такая точка вместе с сопутствующей информацией называется фрагментом.
Фрагмент изменяет соответствующий
ему пиксел в буфере кадра, если он проходит пять тестов.
Каждая вершина вместе
с характеризующими ее данными обрабатывается конвейером OpenGL независимо
и последовательно. Это означает, что каждый примитив будет полностью изображен
до того, как выполнится следующая команда.
Более подробную
функциональную схему конвейера вы можете увидеть в разделе MSDN: Platform SDK/OpenGL/Overview/Introduction
to OpenGL/OpenGL Processing Pipeline.
|
Анимация
На примере
многочисленных хранителей экрана (screen-saver) вы видели, как гладко работает
анимация в OpenGL. OpenGL использует два буфера памяти (front and back). Первый
(front-буфер) отображается на экране, второй в это время может обрабатываться
процессором. Когда обработка закончится, то есть очередная сцена будет готова,
вы можете произвести быстрое переключение буферов (swap), обеспечивая тем самым
гладкую анимацию изображения. При обмене копирование массивов не происходит,
изменяется лишь значение указателя (адреса) отображаемого блока памяти. Отметьте,
что процесс рисования в back-буфер происходит быстрее, чем в front, так как
большинство видеокарт запрещают редактировать изображение в момент вертикальной
развертки, а это происходит 60-90 раз в секунду.
Рассмотрим
основную схему алгоритма анимации, используемого в OpenGL-при-ложениях. В кино
эффект движения достигается тем, что каждый кадр проецируется на экран в течение
короткого промежутка времени, затем шторка проектора моментально закрывается,
пленка продвигается на один кадр, вновь открывается шторка и цикл повторяется.
Период цикла равен 1/24 с или даже 1/48 с в современных кинопроекторах. Современные
компьютеры допускают смену кадра (refresh rate) до 120 раз в секунду. Рассмотренный
алгоритм можно записать так в цикле по всем кадрам:
сотри старое изображение;
создай новое изображение;
задержи изображение на
какое-то время.
Если реализовать
анимацию по такой схеме, то эффект будет тем более удручающим, чем ближе к 1/24
с подходит время создания изображения, так как полное изображение существует
на экране лишь долю периода. Большую часть периода мы видим процесс рисования.
OpenGL предоставляет
возможность двойной буферизации (аппаратной или программной — зависит от видеокарты).
Алгоритм анимации в этом случае таков: пока проецируется первый кадр, создается
второй. Переключение кадров происходит только после того, как закончится формирование
второго кадра. Пользователь никогда не видит незавершенный кадр. Эту схему можно
представить в виде проектора с двумя кадрами пленки. В момент демонстрации первого
второй стирается и вновь рисуется. Новый алгоритм можно записать в цикле по
кадрам:
Сотри старое изображение
в back-буфере.
Создай в нем новое изображение.
Переключи буферы (front-back).
Последний шаг
алгоритма не начнет выполняться, пока не закончится предыдущий шаг — создание
нового кадра в back-буфере. Ожидание этого события (конец рисования в невидимый
буфер) дополняется ожиданием завершения цикла прямого хода развертки экрана.
Поэтому самая большая частота смены изображений равна текущему значению частоты
кадров дисплея. Допустим, что эта частота равна 60 Гц, тогда частота смены изображений
будет 60 fps (frames per second — кадров в секунду). Если время рисования занимает
немногим более 1/60 с (пусть 1/45 с), то один и тот же кадр будет проецироваться
два такта цикла развертки и частота смены изображений реально будет 30 fps.
Промежуток времени между 1/30 с и 1/45 с процессор простаивает (is idle). Если
время подготовки невидимого кадра нестабильно (плавает), то частота смены изображений
может измениться скачком, что воспринимается как неприятная помеха. Для сглаживания
этого эффекта иногда искусственно добавляют небольшую задержку, с тем чтобы
немного снизить частоту, но сделать ее стабильной. Отметьте, что OpenGL не имеет
команды переключения буферов, так как такая команда всегда зависит от платформы.
Мы будем пользоваться функцией SwapBuf f ers(HDC hdc), входящей в состав Windows
API.
|
Другие
функции OpenGL
Другие функции
OpenGL позволяют размещать объекты на трехмерной сцене, выбирать точку размещения
глаза наблюдателя (камеру), передвигать эту точку. Неотъемлемой частью трехмерной
графики является освещение материалов. Конвейер OpenGL использует специальные
алгоритмы подсчета цвета любого фрагмента с учетом заданных свойств материала
и источников света. Моделирование атмосферных эффектов (тумана, дыма, дымки)
делает изображения более реалистичными. Функции моделирования тумана, дыма,
загрязнений или просто эффекта присутствия воздуха можно найти в справочной
системе по ключевому слову Fog.
Механизм anti-aliasing
сглаживает неровные края линий, отображаемых на компьютерном дисплее при низком
графическом разрешении. Anti-aliasing является стандартной техникой в компьютерной
графике и заключается в изменении цвета точек вблизи границ изломов. Техника
Gouraud-тени сглаживает тень трехмерного объекта для достижения тонких различий
цветов на специфической поверхности.
Четвертая составляющая
цветового кода RGBA носит название alpha. Alpha-смешивание позволяет комбинировать
цвет обрабатываемого фрагмента с цветом точек, которые уже хранятся в буфере,
моделируя тем самым прозрачность воздуха, стекла или другого материала. Этот
эффект используют при демонстрации распределения поля внутри замкнутого объема.
Достаточно дать пользователю возможность управлять степенью прозрачности границ,
и он сможет рассматривать результаты вычислений (например, поверхности равного
уровня искомого поля) внутри трехмерных объектов.
OpenGL не предоставляет
разработчику команд для описания сложных моделей. В ней есть примитивные геометрические
объекты: точки, линии и многоугольники. Разработчик должен сам сконструировать
модели, основываясь на этих нескольких простых примитивах. Но есть библиотеки,
например классы Open Inventor, которые реализуют более сложные модели. Разработчик
может использовать эти библиотеки для построения своих собственных.
Конвейер OpenGL
реализует процедурный, а не описательный подход к созданию изображения. Он выполняет
функцию сервера, который обслуживает клиента — приложение, генерирующее последовательность
команд. Коды сервера могут выполняться на другом компьютере, сервер может одновременно
поддерживать несколько контекстов передачи OpenGL, а клиент может подключаться
к любому из них. Однако система Windows полностью контролирует память, отводимую
под буфер кадра (frame buffer). OpenGL может вступить в игру только после того,
как Windows создаст и подготовит окно для передачи (rendering) изображения OpenGL.
|
Контекст
передачи изображения
Окно OpenGL
имеет свой собственный формат пикселов. Необходимым условием ее работы является
установка pixel-формата экранной поверхности в контексте устройства HDC, а следовательно,
и в окне, с которым он связан. Формат устанавливается один раз, повторная установка
недопустима, так как может привести к сбоям в работе подсистемы управления окнами
(Windows Manager). После установки формата (вызов SetPixelFormat) следует создать
контекст передачи изображения OpenGL, описатель которой имеет тип HGLRC. Контекст
передачи (rendering context) создается функцией wglCreateContext с учетом выбранного
формата пикселов. Контекст передачи изображения — это связь OpenGL с Windows.
Создание этого контекста требует, чтобы обычный контекст существовал и был явно
указан в параметре wglCreateContext. Контекст HGLRC использует тот же формат
пикселов, что и HDC.
Несмотря на
сходство, эти контексты различны. HDC содержит информацию относящуюся к функциям
GDI, a HGLRC — к функциям OpenGL. Поток, вызывающий функции OpenGL, должен предварительно
объявить контекст передачи текущим (current). Иначе вызовы не будут иметь эффекта.
Уничтожать контекст передачи надо после отсоединения его от потока. Несколько
контекстов передачи могут одновременно рисовать в окне OpenGL, но только один
из них (тот, который ассоциирован с HDC) может быть текущим или активным в потоке.
Для описания
формата пикселов экранной поверхности в OpenGL используется структура PIXELFORMATDESCRIPTOR.
Прежде всего pixel format — это описание цветового режима, действующего в данном
окне. Например, если видеокарта может работать в режиме передачи 256 цветов,
то для кодирования цвета каждого пиксела в этом режиме необходимо иметь 8 бит
памяти. В этом случае говорят о 8-битовой глубине поверхности рисования или
окна OpenGL. Иногда в таком случае говорят о 8-битовой глубине цвета. Существуют
режимы с 15-битовой глубиной (32 768 цветов), 16-битовой (65 536 цветов), 24-битовой
(16 миллионов цветов). Выбор формата зависит от возможностей карты и намерений
разработчика. Кроме глубины цвета к pixel-формату относятся такие настройки,
как:
тип буферизации (одинарная
или двойная);
схема образования цвета
(RGBА или на основе палитры);
количество бит для буфера
глубины, то есть буфера Z-координат изображения (ось Z считается направленной
из глубины экрана к наблюдателю);
поддержка регулировки
прозрачностью (alfa) и др.
Вы можете выбрать
один из более чем 20 готовых pixel-форматов или задать произвольную комбинацию
параметров и попросить найти ближайшую возможную ее реализацию. Microsoft GDI-реализация
OpenGL вносит свои коррективы в возможные варианты реализации pixel-формата,
а аппаратная поддержка меняется в зависимости от производителя и может значительно
расширить его возможности. Каждое окно OpenGL должно иметь свой собственный
pixel-формат.
По умолчанию
Windows не вырезает (в смысле перерисовки) дочерние окна из клиентской
области родительского окна, поэтому при создании окнам OpenGL следует задать
бит стиля WS_CLIPCHILDREN. В этом случае система не позволяет рисовать родительскому
GDI-окну в пределах дочернего окна OpenGL. Несколько окон OpenGL, каждое со
своим форматом, могут быть отображены одновременно, поэтому необходимо установить
еще один бит стиля WS_CLIPSIBLINGS, чтобы предотвратить рисование в окне соседа.
Для окон OpenGL недопустим стиль CS_PARENTDC (CM. MSDN).
|
Подготовка
окна
Подготовку
контекста передачи OpenGL надо рассматривать как некий обязательный ритуал,
в котором порядок действий давно определен и без которого нельзя начинать творческую
работу по созданию сцены OpenGL. Стоит где-то промахнуться, и вы увидите молчаливый
белый экран. Сначала надо подготовить окно так, чтобы вызовы функций OpenGL
начали работать. В этой процедуре выделяют следующие шаги:
установка стиля окна;
обработка сообщения WM_ERASEBACKGROUND
и отказ от стирания фона;
установка pixel-формата;
создание контекста устройства
(НОС) и контекста передачи (HGLRC);
специфическая обработка
сообщения WM_SIZE;
обработка сообщения WM_PAINT;
освобождение контекстов
при закрытии окна.
Чтобы использовать
функции библиотеки OpenGL в вашем приложении, надо убедиться, что в системном
каталоге Windows присутствуют модули OpenGL32.dll и GLU32.dll. Они должны быть
там, так как компания Silicon Graphics (авторы пакета OpenGL) постаралась, чтобы
поддержка OpenGL на платформе Windows была максимально доступна и достаточно
надежна. Однако хочу предупредить, что я встречал системы, в которых контекст
передачи (rendering context) OpenGL работает ненадежно — появляются пятна пробелов
и задержка перерисовки, если работа идет не в полноэкранном режиме. Если это
есть, то должно проявляться при запуске любой программы, использующей OpenGL.
Причина, видимо, в драйвере видеопамяти.
Типы данных
OpenGL использует
свои собственные типы данных, которые должны соответствовать аналогичным типам
той платформы, на которой библиотека установлена. В Microsoft-реализации соответствие
типов задано в файле заголовков GL.H так, как показано ниже. Эта таблица понадобится
вам при анализе примеров и при разработке собственного кода:
typedef
unsigned int GLenum;
typedef
unsigned char GLboolean;
typedef
unsigned int GLbitfield;
typedef
signed char GLbyte;
typedef
short GLshort;
typedef
int GLint;
typedef
int GLsizei;
typedef
unsigned char GLubyte;
typedef
unsigned short GLushort;
typedef
unsigned int GLuint;
typedef
float GLfloat;
typedef
float GLclampf;
typedef
double GLdouble;
typedef
double GLclampd;
typedef
void GLvoid;
|
Создание
консольного проекта
Для исследования
возможностей функций библиотек OpenGL целесообразно создать простой проект консольного
типа, в котором для работы с другим (Windows) окном будут использованы функции
дополнительной библиотеки OpenGL, описанной в файле GLAUX.LIB. Рассмотрим последовательность
шагов для создания нового проекта консольного типа.
На странице VS Home Page
выберите команду Create New Project и в окне появившегося диалога New Project
выберите тип проекта Visual C++ Projects, а в качестве шаблона (в поле Templates)
— Managed C++ Empty Project.
Задайте имя проекта
Console, желаемое местоположение папки с проектом и нажмите ОК.
Поставьте фокус на элемент
Console в окне Solution Explorer, вызовите контекстное меню и выберите команду
Add > Add New Item.
В окне диалога Add New
Item перейдите в список Templates и выберите строку C++File(.cpp).
В поле Name того же диалога
задайте имя файла OG.cpp и нажмите кнопку Open.
Далее вы будете
вводить код в окно редактора Studio.Net (вкладка OG.cpp). Для того чтобы компоновщик
подключил все библиотеки OpenGL, произведите настройку проекта.
Поставьте фокус на элемент
Console в окне Solution Explorer и дайте команду Project > Properties или
ее эквивалент View t Property Pages.
В окне открывшегося диалога
Console Property Pages выберите элемент дерева Linker * Input.
Переведите фокус в поле
Additional Inputs окна справа и добавьте в конец существующего текста имена
файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB GLAUX.LIB. Убедитесь
в том, что все имена разделены пробелами и нажмите ОК.
В новый пустой
файл OG.cpp поместите следующий код приложения, которое для создания Windows-окна
пользуется услугами библиотеки GLAUX.LIB. Для этого необходимо к проекту консольного
типа подключить файл Windows.h1:
#include
<Windows.h>
#include
<math.h>
//======
Подключение заголовков библиотек OpenGL
#include
<GL\gl.h>
#
include <GL\glu.h>
#include
<GL\Glaux.h>
//=====Макроподстановка
для изображения одной линии
#define
Line(xl,yl,x2,y2) \
glBegin(GL_LINES);
\
glVertex2d
( (xl), (yl)); \
glVertex2d
((x2),(y2)); \
glEnd()
;
//======
Реакция на сообщение WM_PAINT
void
_stdcall OnDraw()
{
//======
Стираем буфер кадра (framebuffer)
glClear
(GL_COLOR__BUFFER_BIT) ;
//======
Выбираем черный цвет рисования
glColorSf
(0., 0., 0.);
//===
В 1-м ряду рисуем 3 линии с разной штриховкой
glEnable
(GL_LINE_STIPPLE);
glLineWidth
(2.);
glLineStipple
(1, 0x0101); // dot
Line
(50., 125., 150., 125.);
glLineStipple
(1, OxOOFF); // dash
Line
(150., 125., 250., 125.);
glLineStipple
(1, OxlC47); // dash/dot/dash
Line
(250., 125., 350., 125.);
//======
Во 2-м ряду то же, но шире в 6 раз
glLineWidth
(6.);
glLineStipple
(1, 0x0101); // dot
Line
(50., 100., 150., 100.);
glLineStipple
(1, OxOOFF); // dash
Line
(150., 100., 250., 100.);
glLineStipple
(1, OxlC47); // dash/dot/dash
Line
(250., 100., 350., 100.);
//==
Во 3-м ряду 7 линий являются частями
//==
полосы (strip). Учетверенный узор не прерывается
glLineWidth
(2.);
glLineStipple
(4, OxlC47); // dash/dot/dash
glBegin
(GL_LINE_STRIP);
for
(int i =1; i < 8; i++)
glVertex2d
(50.*i, 75.); glEnd() ;
//==
Во 4-м ряду 6 независимых, отдельных линий
//==
Тот же узор, но он каждый раз начинается заново
for
(i = 1; i < 7; i++)
{
Line
(50*1, 50, 50* (i+1), 50);
}
//======
во 5-м ряду 1 линия с тем же узором
glLineStipple
(4, OxlC47); // dash/dot/dash
Line
(50., 25., 350., 25.);
glDisable
(GL_LINE_STIPPLE); glFlush ();
}
//=====
Реакция на WM_SIZE
void
_stdcall OnSize (int w, int h)
{
glViewport
(0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode
(GL_PROJECTION); glLoadldentity();
//======
Режим ортографической проекции
gluOrtho2D
(0.0, double(w), 0.0, double(h));
}
//======
Настройки
void
Init()
{
//======
Цвет фона - белый
glClearColor
(1., 1., 1., 0.);
//======
Нет интерполяции цвета при растеризации
glShadeModel
(GL_FLAT); }
void
main()
{
//===
Установка pixel-формата и подготовка окна OpenGL
auxInitDisplayMode
(AUX_SINGLE | AUX_RGB);
auxInitPosition
(200, 200, 550, 250);
auxInitWindow("My
Stipple Test");
Init()
;
auxReshapeFunc
(OnSize);
//
Кого вызывать при WM_SIZE auxMainLoop(OnDraw);
//
Кого вызывать при WM_PAINT
}
Функция main
содержит стандартную последовательность действий, которые производятся во всех
консольных приложениях OpenGL. С ней надо работать как с шаблоном приложений
рассматриваемого типа. Первые три строчки функции main устанавливают pixel-формат
окна OpenGL. Заботу о его выборе взяла на себя функция auxInitDisplayMode из
вспомогательной библиотеки. В параметре мы указали режим использования только
одного (front) буфера (бит AUX_SINGLE) и цветовую схему без использования палитры
(бит AUX_RGB).
В функции init
обычно производят индивидуальные настройки конечного автомата OpenGL. Здесь
мы установили белый цвет в качестве цвета стирания или фона окна и режим заполнения
внутренних точек полигонов. Константа GL_FLAT соответствует отсутствию интерполяции
цветов. Вызов функции auxReshapeFunc выполняет ту же роль, что и макрос ON_WM_SIZE
в MFC-приложении. Происходит связывание сообщения Windows с функцией его обработки.
Все функции обработки должны иметь тип void _stdcall. Вы можете встретить и
эквивалентное описание этого типа (void CALLBACK). Имена функций OnDraw и OnSize
выбраны намеренно, чтобы напомнить о Windows и MFC. В общем случае они могут
быть произвольными. Важно запомнить, что последним в функции main должен быть
вызов auxMainLoop.
В OnSize производится
вызов функции glviewport, которая задает так называемый прямоугольник просмотра.
Мы задали его равным всей клиентской области окна. Конвейер OpenGL использует
эти установки так, чтобы поместить изображение в центр окна и растянуть или
сжать его пропорционально размерам окна. Аффинные преобразования координат производятся
по формулам:
Xw=(X+1)(width/2)+X0
Yw=(Y+1)(height/2)+Y0
В левой части
равенств стоят оконные координаты:
(X, Y) — это координаты
изображаемого объекта. Мы будем задавать их при формировании граничных точек
линий командами glvertex2d;
(Хо, Yo) — это координаты
левого верхнего угла окна. Они задаются первым и вторым параметрами функции
glviewport;
сомножители в формуле
(width и height) соответствуют третьему и четвертому параметрам (w, h) функции
glviewport и равны текущим значениям размеров окна.
Как видно из
подстановки в формулу, точка с координатами (0,0) попадет в центр окна, а при
увеличении ширины или высоты окна (width или height) координаты изображения
будут увеличиваться пропорционально. Вызов
glMatrixMode
(GL_PROJECTION);
определяет
в качестве текущей матрицу проецирования, а вызов glLoadldentity делает ее равной
единичной матрице. Следующий за этим вызов
gluOrtho2D
(0.0, double(w), 0.0, double(h));
задает в качестве
матрицы преобразования матрицу двухмерной ортографической (или параллельной)
проекции. Изображение будет отсекаться конвейером OpenGL, если его детали вылезают
из границ, заданных параметрами функции gluOrtho2D.
|
Штриховка
линий
Основные действия
разворачиваются в функции перерисовки. Здесь мы рисуем несколько линий, изменяя
узор их штриховки. Режим штриховки линий включается командой glEnable (GL_LINE_STIPPLE).
Узор штриховки задается параметрами функции glLineStipple. Первый параметр является
коэффициентом повторения, а второй определяет сам узор. Он должен быть 16-битовой
константой или переменной, последовательность бит которой определяет последовательность
фрагментов при растеризации линии. Порядок использования битов возрастающий,
то есть нулевой бит используется первым. Каждому пикселу соответствует один
бит, что характеризует ситуацию: текущий цвет либо включен (бит равен 1), либо
выключен (бит равен 0). Алгоритм станет очевидным, если вы запустите приложение
(Ctrl+F5), устраните возможные ошибки и увидите результат. Обратите внимание
на различие штриховки в 3-й и 4-й строках. Попытайтесь объяснить различие. Создайте
несколько своих собственных узоров штриховых (stippled) линий.
|
Штриховка
полигонов
Теперь применим
штриховку (stipple) к полигонам. Режим штриховки включается и выключается стандартным
способом:
glEnable
(GL_POLYGON_STIPPLE) ;
glDisable
(GL_POLYGON_STIPPLE);
Bitmap-узор
(pattern) штриховки надо предварительно подготовить в массиве такой размерности,
чтобы заполнить bitmap площадью 32x32 = 1024 пиксела. Размерность массива с
узором определяется так: 1024 бита можно разместить в 128 переменных по одному
байту. Мы разместим их в 16 строках по 8 байт. Имеем 16 х х 8 х 8 = 1024 бита
(или пиксела).
Функцию OnDraw
замените целиком. Так же поступайте и дальше. Следите лишь за изменениями в
функциях main, OnSize и init, которые понадобятся при радикальной смене режима
передачи (rendering). Позже мы перейдем к передаче трехмерной графики, а пока
режим тот же — двухмерная графика:
void
_stdcall OnDraw()
{
//======
Стираем окно
glClear
(GL_COLOR_BUFFER_BIT);
//======
Цвет фона (синеватый)
glColor3f
(0.3f, 0.3f, 1.);
//==
Рисуем сначала unstippled rectangle (без узора)
//==
Rect - это тоже полигон
glRectf
(20., 20., 115., 120.);
glColor3f
(1., 0., 0.); // Меняем цвет на красный
glEnable
(GL_POLYGON_STIPPLE); // Включаем штриховку
glPolygonStipple
(gStrip); // Задаем узор
glRectf
(120., 20., 215., 120.); // Рисуем
glColorSf
(O.,0.,0.); // Меняем цвет на черный
glPolygonStipple
(gSpade);
//
Меняем узор glRectf (220., 20., 315., 120.);
glPolygonStipple
(gStrip); // Меняем узор
glColor3f
(0., 0.6f, 0.3f);
glRectf
(320., 20., 415., 120.);
//==
Готовимся заполнить более сложный, невыпуклый
//==
(nоn convex) полигон
glPolygonStipple
(gSpade);
glColorSd
(0.6, O.f, 0.3f);
//=======
Шесть вершин по три координаты
float
c[6][3] =
{
420.,120.,0.,
420.,70.,0.,
470.,20.,0.,
520., 70.,0.,
520.,120.,0.,
470.,100.,0.
};
//==
Здесь мы специально выбираем nоn convex полигон,
//==
чтобы увидеть как плохо с ним обходится OpenGL
glBegin
(GL_POLYGON) ;
for
(int i=0; i<6; i++)
glVertex3fv(c[i]
) ;
glEnd()
;
glDisable
(GL_POLYGON_STIPPLE) ;
glFlush
();
}
Запустите и
убедитесь в том, что последний полигон потерял одну точку. Затем замените цикл
задания его вершин на:
for
(int i=5;
i>=0; i--) glVertex3fv(c[i]) ;
Здесь мы изменили
порядок обхода вершин и начали с вогнутой вершины. Запустите и убедитесь в том,
что теперь в полигоне есть все шесть вершин. OpenGL не гарантирует точную передачу
вогнутых полигонов. Поэтому для надежной передачи их надо предварительно разбивать
на выпуклые части. Если этими частями будут треугольники, то процесс разбиения
называется tessellation (мозаика). Есть специальные функции для тесселяции полигонов.
Их рассмотрение выходит за рамки этой книги. Попробуйте самостоятельно задать
рассмотренный выше полигон в виде двух выпуклых четырехугольников. Для этого
посмотрите справку по функции glBegin с параметром GL_QUADS.
Полигоны можно
рисовать либо закрашенными (режим — GL_FILL), либо в скелетном виде (GL_LINE),
либо в виде намеков (GL_POINT). Испробуйте все режимы на примере невыпуклой
звезды. При рисовании точками попробуйте предварительно дать команду glPointSize
(5):
void
_stdcall OnDraw()
{
glClear
(GL_COLOR_BUFFER_BIT);
glColor3d
(1., 0.4, 1.);
//===
2 угла, характеризующие звезду и
//===
2 характерные точки
double
pi = 4. * atan(l.),
al
= pi / 10., a2 = 3. * al,
xl
= costal), yl = sin(al)',
x2
= cos(a2), y2 = sin(a2);
//===
Мировые координаты вершин нормированной звезды
double
с[5][3] =
{
0.,
1., 0.,
-х2,
-у2, 0.,
xl,
yl, 0.,
-xl,
yl, 0.,
х2,
-у2, 0.,
};
//======
Оконные координаты
for
(int i=0; i<5; i+t)
{
c[i][0]
= 250 + 100*c[i][0];
c[i][l]
= 100 + 100*c[i] [1] ;
}
//===
Режим заполнения полигона - скелетный
glPolygonMode(GL_FRONT_AND_BACK,
GL_LINE);
//===
Задаем вершины полигона
glBegin(GL_POLYGON);
for
(i=0; i<5; i++)
glVertex3dv(c[i]
) ;
glEnd()
;
glFlush()
;
}
|
Как
убирать внутренние линии
Каждой вершине
по умолчанию присваивается булевский признак (флаг) того, что из нее может выходить
видимое ребро (линия). Если надо отменить рисование линии, например скрыть тесселяцию
вогнутого полигона, то можно снять флаг ребра командой glEdgeFlag(GL_FALSE);
для текущей вершины. Затем можно вновь установить его, когда дело дойдет до
ребер, которые должны быть видимы — команда glEdgeFlag(GL_TRUE);. Попробуйте
самостоятельно вставить флаги ребер в следующем фрагменте программы так, чтобы
скрыть линию соединения двух четырехугольников. Попробуйте затем заменить алгоритм
так, чтобы изобразить ту же фигуру в виде одного полигона:
void
_stdcall OnDraw()
{
glClear
(GL_COLOR_BOFFER_BIT); glColorSd (1., 0.4, 1.);
//======
Вогнутый шестиугольник, но мы зададим его
//======
в виде двух четырехугольников
float
с[6][3] =
{
200.
200.,0.,
200.
100.,0.,
250.
20.,0.,
300.
100.,0.,
300.
200.,0.,
250.
100.,0.,
};
glPolygonMode(GL_FRONT_AND_BACK,
GL_LINE);
glBegin(GL_QUADS);
glVertex3fv(c[5])
glVertex3fv(c[0])
glVertex3fv(c[l])
glVertex3fv(c[2])
glVertex3fv(c[5])
glVertex3fv(c[2])
glVertex3fv(c[3])
glVertex3fv(c[4])
glEnd();
glFlush
();
}
|
Перспективная проекция
В ортографической
проекции .(giuOrtho2D) мы, в сущности, создавали двухмерные изображения в плоскости
z = 0. В других типах проекций (gldrtho и gluPerspective) можно создавать трехмерные
изображения. Эффект реального трехмерного пространства достигается в проекции
с учетом перспективы. Теперь мы будем пользоваться только этим режимом передачи.
Другой режим glOrtho вы опробуете самостоятельно, так как я не вижу какой-либо
интересной сферы его применения. Вставьте в обработчик WM_SIZE вместо строки:
gluOrtho2D
(0., double (w), 0., double(h) ) ;
строку:
gluPerspective(45.,
double(w)/double(h), 1., 100.);
В OpenGL для
обозначения видимого объема используется термин frustum. Он имеет латинское
происхождение и примерно означает «отломанная часть, кусок».
Frustum задается
шестью плоскими границами типа (min, max) для каждой из трех пространственных
координат. В перспективном режиме просмотра frustum — это усеченная пирамида,
направленная на наблюдателя из глубины экрана. Все детали сцены, которые попадают
внутрь этого объема, видны, а те, что выходят за него, — отсекаются конвейером
OpenGL. Другой режим просмотра — ортографический, или режим параллельной проекции,
задается с помощью функции glOrtho. Он не учитывает перспективу, то есть при
увеличении (удалении) координаты Z объекта от точки, в которой располагается
наблюдатель, размеры объектов и углы между ними не изменяются, что напоминает
плоские проекции объекта. Первый параметр функции gluPerspective задает угол
перспективы (угол обзора). Чем он меньше, тем больше увеличение. Вспомните школьные
объяснения работы телескопа или бинокля, где были настройки фокусного расстояния,
определяющего угол зрения. Последние два параметра задают переднюю и заднюю
грани видимого объема или frustum'a. Он определяет замкнутый объем, за пределами
которого отсекаются все элементы изображения. Смотри иллюстрации в MSDN / Periodicals
/ Periodicals 96 / Microsoft System Journals/November / OpenGL Without the Pain.
Боковые грани фрустума определяются с учетом дисбаланса двух размеров окна (отношения
double(w) / double(h)). Мы вычисляем его и подаем на вход функции в качестве
второго параметра.
Вспомните и
найдите функцию, в которой мы задавали размеры окна, и увеличьте вертикальный
размер до 500, так как далее мы собираемся изображать более крупные объекты.
Введите определения новых глобальных переменных:
//======
Углы поворотов изображения вокруг осей X и Y
double
gdAngleX, gdAngleY; //====== Сдвиги вдоль координат
double
gdTransX, gdTransY, gdTransZ = -4.;
С их помощью
мы будем транслировать (перемещать) изображения в трехмерном пространстве и
вращать их вокруг двух осей. Включите учет глубины, вставив вызов
glEnable(GL_DEPTH_TEST);
в функцию Init.
Туда же вставьте установку режима заполнения полигонов
glPolygonMode(GL_FRONT_AND_BACK,
GL_FILL);
и уберите строку,
задающую текущий цвет вершин
glColorSd
(1., 0.4, 1.);
так как мы
теперь будем задавать его в другом месте. При подготовке окна OpenGL и формата
его пикселов надо установить бит AUX_DEPTH — учет буфера глубины. Замените существующий
вызов функции auxlnitDisplayMode на: auxInitDisplayMode (AOX_SINGLE I AUX_RGB
I AUX_DEPTH);
В функции перерисовки,
приведенной ниже, мы создадим куб, координаты которого будем преобразовывать
с помощью матрицы моделирования. Порядок работы с этой матрицей таков:
Сначала с помощью команды
glMatrixMode (GL_MODELVIEW); матрица моделирования выбирается в качестве текущей.
Обычно при этом она сразу инициализируется единичной матрицей (команда glLoadldentity).
После этого текущая (единичная)
матрица последовательно домножается справа на матрицы преобразования системы
координат, которые формируются с помощью команд glTranslate* (сдвиги), glRotate*
(вращения) или glScale* (растяжения-сжатия).
Наконец, команды glVertex*
генерируют вершины примитивов, то есть координатные векторы точек трехмерного
пространства. Векторы умножаются (справа) на текущую матрицу моделирования
и тем самым претерпевают такие преобразования, чтобы соответствовать желаемому
местоположению и размерам в сцене OpenGL.
Предположим,
например, что текущая (current) матрица С размерностью 4x4 равна единичной С
= 1 и поступает команда glTranslated (dx, dy, dz);. Эта команда создает матрицу
сдвига Т и умножает ее справа на текущую (единичную) матрицу (С = I*Т). Затем
она вновь записывает результат в текущую матрицу С. Теперь текущая матрица приняла
вид:
Если после
этого дать команду glVertexSd (x, у, z); то координаты точки (х, у, z) преобразуются
по правилам умножения матрицы на вектор:
Примечание
Вы должны помнить, что
вершины всех примитивов в OpenGL заданы 4-ком-— понентным вектором (х, у,
z, w). По умолчанию нормирующий компонент w-1. При работе с двухмерными изображениями
мы для всех вершин задаем координату z = 0. Обратите внимание на то, как четвертый
компонент w помогает производить преобразования, в нашем случае сдвиг, а команда
glTranslate* учитывает координаты сдвигов вдольтрех пространственных осей
(dx, dy, dz).
Команды вращения
glRotate* и растяжения-сжатия glScale* действуют сходным образом. В функции
onDraw, приведенной ниже, начальный поворот и последующие вращения вокруг оси
Y осуществляются вызовом glRotated (gdAngleY, 0., l., 0.);. Аналогичный вызов
glRotated (gdAngleX, 1., 0., 0.); вращает все точки примитивов вокруг оси X:
void
_stdcall OnDraw()
{
glClear(GL_COLOR_BOFFER_BIT
I GL_DEPTH_BUFFER_BIT);
//==
Будем пользоваться услугами матрицы моделирования glMatrixMode <GL_MODELVIEW);
glLoadldentity
();
//===
Задаем смещение координат точек будущих примитивов glTranslated(gdTransX, gdTransY,
gdTransZ);
//===Задаем
вращение координат точек будущих примитивов
glRotated(gdAngleY,
0.,1.,0.);
glRotated(gdAngleX,
1.,0.,0.);
//======
Координаты точек куба (центр его в нуле)
static
float v[8][3] =
{
-1,
1.,-1., //4 точки задней грани задаются
1.,
1., -1., //в порядке против часовой стрелки
1-,
-1-, -1.,
-1,
-1., -1.,
-1,
1,, 1., //4 фронтальные точки
-1-,
-1., 1.,
1,
-1., 1.,
1,
1., 1.
};
//======
6 нормалей для 6-ти граней куба
static
double norm[6][3] =
{
0.,
0., -1., // Rear
0.,
0., 1., // Front
-1.,
0., 0., // Left
1.,
0., 0., // Right
0.,
1., 0., // Top
0.,
-1., 0. // Bottom
};
//======
Индексы вершин
static
GLuint id[6][4] =
{
0,1,2,3,//
Rear (обход CCW - counterclockwise)
4,5,6,7,
// Front
0,3,5,4,
// Left
7,6,2,1,
// Right
0,4,7,1,
// Top
5,3,2,
6, // Bottom
};
glPolygonMode(GL_FRONT_AND_BACK,
GL_FILL);
glColorSd
(1., 0.4, 1.);
glBegin(GL_QUADS);
//======
Долго готовились - быстро рисуем
for
(int i = 0; i < 6; i++)
{
glNormal3dv(norm[i])
;
for
(int j = 0; j < 4; j++)
glVertex3fv(v[id[i]
[j]]);
}
glEnd()
;
glFlush
();
}
Запустите и
отладьте приложение. Вы должны увидеть совсем плоский квадрат, несмотря на обещанную
трехмерность объекта. Пока ничего вразумительного, никакого трехмерного эффекта.
Закомментируйте или удалите (или измените на GL_SMOOTH) настройку glShadeModel
(GL_FLAT), так как теперь мы хотим интерполировать цвета при изображении полигонов.
Это работает при задании разных цветов вершинам. Попробуйте задать всем вершинам
разные цвета.
Попробуйте
покрутить изображение, изменяя значения переменных gdAngleX, gdAngleY. Например,
вместо нулевых значений, присваиваемых глобальным переменным по умолчанию, задайте:
double
gdAngleX=20, gdAngleY=20;
Посмотрите
в справке смысл всех параметров функции glRotated и опробуйте одновременное
вращение вокруг двух осей, задав большее число единиц в качестве параметров.
Позже мы автоматизируем процесс сдвигов и вращений, а сейчас, пока мы не умеем
реагировать на сообщения мыши, просто измените значение какого-либо угла поворота
и запустите музыку. Объясните результаты. Попробуйте отодвинуть изображение,
изменив регулировку gdTransZ. Объясните знак смещения.
|
Вносим
свет
Пока нет освещения,
все попытки внести трехмерный реализм обречены на неудачу. Свет отражается по
нормали (перпендикуляру) к поверхности. Однако в OpenGL нормаль надо задавать
в вершинах, так как в случае произвольной криволинейной поверхности направление
нормали различно в каждой ее точке. Чем точнее вычислен вектор нормали, тем
реалистичней изображение. Но это дело довольно тонкое. Для тех, кто не любит
математику, то есть излишне напрягать свое мышление, — просто отвратительное.
Примеры с автонормалями расслабляют и усыпляют бдительность, так как они скрывают
детали реализации. Чтобы с ними работать, тоже надо прилагать усилия и правильно
включать вычислители (evaluators). Смотри документацию по функциям giMap*. В
нашем же случае все просто. Нормали уже вычислены, осталось включить свет. Сделайте
это, вставив изменения в тело функции init. Включите еще два параметра в конечном
автомате (state machine) OpenGL.
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
Задайте некоторый
поворот, например double gdAngleX=15, gdAngleY=30, и запустите на выполнение.
Изображение должно стать значительно лучше, но куда делся цвет куба? Свет исключил
цвет. Дело в том, что теперь цвет каждого пиксела вычисляется по формуле, которая
учитывает цвет материала поверхности, его отражающие и испускающие свойства,
цвет самого света, его направление и законы распространения (точнее, затухания
— attenuation). По умолчанию OpenGL учитывает только направление света, но не
место расположения источника. По умолчанию же свет направлен вдоль оси Z. Обратите
внимание на то, что индекс 0 в GL_LIGHTO означает, что мы включаем первый из
GL_MAX_LIGHTS возможных источников света. Эта константа зависит от платформы.
Давайте определим ее для нашей платформы. Вставьте такой фрагмент:
int
Lights;
glGetIntegerv(GL_MAX_LIGHTS, &Lights);
_asm
nор
внутрь функции Init (после строки glEnable(GL_LIGHTO);) и поставьте точку останова
(F9) на строке __asm пор.
Примечание
Ассемблерная вставка _asm
пор упрощает просмотр значения переменных в окне Variables, так как не дает
новых (и отвлекающих) элементов просмотра. Идея использования такого приема
принадлежит Марине Полубенцевой, с которой мы сотрудничаем в Microsoft Authorized
Education Center при ФПК СПбГТУ (www.Avalon.ru). В книге использовано еще
несколько идей и технологических приемов, автором которых является Полубенцева.
Затем нажмите F5 (Go). Когда выполнение дойдет до точки останова, посмотрите
в окно Variables и убедитесь в том, что Lights приняла значение 8. Если хотите,
то используйте описанный прием в дальнейшем для выяснения многочисленных параметров
и состояний OpenGL. Посмотрите справку по glGet, чтобы получить представление
о количестве этих параметров. Теперь уберите отладочный код и включите еще один
тумблер в машине состояний OpenGL — учет цвета материала. Для этого вставьте
строку:
glEnable(GL_COLOR_MATERIAL) ;
в функцию Init и запустите приложение. Обратите внимание на отличие оттенков
цвета разных граней. Они определяются OpenGL с учетом направления нормалей.
Попробуйте изменить их направление и посмотрите, что получится.
|
Интерактивное
управление положением и ориентацией
Теперь хочется
рассмотреть трехмерный объект с разных сторон. Удобнее это делать с помощью
мыши. Документация MSDN содержит весьма скудные сведения относительно aux-функций,
но в примерах все же можно найти какую-то информацию. Оказывается для введения
реакции на мышиные события надо ввести в main следующие строки и, конечно, написать
функции обработки
auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSELOC,OnLMouseMove);
auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSELOC,OnRMouseMove); auxMouseFunc(AUX_LEFTBUTTON,AUX_MOUSEDOWN,OnButtonDown);
auxMouseFunc(AUX_RIGHTBUTTON,AUX_MOUSEDOWN,OnButtonDown);
Обратите внимание,
что дих разделяет WM_MOUSEMOVE на две (в общем случае на три) кнопки. Это нам
как раз подходит, так как мы хотим левой кнопкой вращать, а правой удалять-приближать
(делать zooming) изображение. События отпускания кнопок нам не понадобились
по причине того, что обработчики AUX_MOUSELOC (читай WM_MOUSEMOVE) вызываются
только в случае, если соответствующие кнопки нажаты. Поэтому не нужно поднимать
и отпускать флаг захвата объекта мышью. Именно это нам и нужно. Как легко, когда
кто-то все продумал! Мы не делаем различия между нажатиями левой и правой кнопки,
так как задача у них общая — запомнить текущие координаты указателя мыши. Вставьте
декларации четырех функций, а затем приступим к их созданию. Так как сейчас
мы не имеем классов для инкапсуляции переменных состояния мыши, то придется
добавить глобальные переменные:
int
giX, giY; // Текущая позиция указателя мыши
Тела глобальных
функций обработки вы должны вставить до того места, в котором они вызываются.
Алгоритм изменения параметров gdAngleX, gdAngleX и gdTransZ очевиден, но обратите
внимание на детали. Например, как добывать координаты курсора мыши. Их присылает
система, a AUX хранит их в структуре data, информацию о которой вы можете получить
разве что в файле заголовков Glaux.h:
static
void _stdcall OnButtonDown(AUX_EVENTREC
*pEvent)
{
//======
Запоминаем координаты мыши
giX
= pEvent->data[AUX_MOUSEX];
giY
= pEvent->data[AUX_MOUSEY];
}
static
void _stdcall OnLMouseMove(AUX_EVENTREC *pEvent)
{
//======
Узнаем текущие координаты
int
x = pEvent->data[AUX_MOUSEX];
int
у = pEvent->data[AUX_MOUSEY];
//======
Изменяем углы поворота пропорционально
//======
смещению мыши
gdAngleX
+= (у - giY)/10.f;
gdAngleY
+= (x - giX)/10.f;
//======
Запоминаем координаты мыши
giX
= x; giY = у; >
Static
void _stdcall OnRMouseMove(AUX_EVENTREC *pEvent)
int
x = pEvent->data[AUX_MOUSEX];
int
у = pEvent->data[AUX_MOUSEY] ;
//=====<=
На сколько удалить или приблизить
double
dx = (x - giX)/200.f;
double
dy = (y - giY)/200.f;
//======
Удаляем или приближаем
gdTransZ
+= (dx + dy)/2.f;
//======
Запоминаем координаты мыши
giX
= x; giY = y;
}
Запустите и
опробуйте. Кубик должен управляться, но в обработке мышиных событий присутствует
явная ошибка. Для того чтобы ее увидеть, нажмите правую кнопку и выведите курсор
мыши за пределы окна влево. Изображение исчезло. один из слушателей наших курсов
(Халип В. М. E-mail: viktor@mail.ru) самостоятельно нашел объяснение этому казусу
и устранил дефект. Для того чтобы обнаружить его, вставьте в список директив
препроцессора еще одну — #include <stdio.h>, а в функцию OnRMouseMove
— вызов printf ("\n%d",x);. Теперь координата курсора мыши будет выводиться
в текстовое окно консольного приложения. Повторите опыт с правой кнопкой и убедитесь
в том, что при выходе за пределы окна (влево), координата х получает недопустимое
значение (>65000). Для устранения дефекта достаточно заменить строки:
int
x = pEvent->data[AUX_MOUSEX];
int
у = pEvent->data[AUX_MOUSEY];
на
short
x = pEvent->data[AUX_MOUSEX];
short
у = pEvent->data[AUX_MOUSEY];
в функциях
OnLMouseMove и OnRMouseMove. Теперь повторите опыт и убедитесь в том, что, переходя
через границу окна, координата х изменяется монотонно и приобретает отрицательные
значения. Чтобы быть последовательным, замените тип глобальных данных для хранения
текущей позиции курсора мыши. Вместо int giX, giY; вставьте short giX, giY;.
Объяснение эффекта мы оставляем читателю в качестве упражнения по информатике.
|
Двойная
буферизация
В настоящий
момент перерисовка изображения во время манипуляций мышью очень плохая, так
как мы работаем с одним (front) буфером. Пора подключать второй. Вместо вызова
glFlush; вставьте вызов функции auxSwapBuffers();
J- из другой
библиотеки, которая, как вы помните, не документирована. Но этого мало — надо
заменить волшебное слово SINGLE на не менее волшебное слово —DOUBLE. Местоположение
вычислите самостоятельно. Поиск места вынуждает прокручивать в голове последовательность
вызовов функций, что является полезным, а для многих и необходимым упражнением.
После этого запустите приложение и отметьте, что управляемость кубика улучшилась,
но при достаточно большом его повороте вокруг оси Y поворот вокруг оси X ведет
себя так, как будто сама ось «повернута». Если вы поменяете порядок вызова двух
функций вращения glRotated, то эффект останется, но проявит себя в симметричном
варианте. Исправьте это, если хотите. Хорошая задача на сообразительность, так
как не требует специфических знаний языка программирования, а только общих представлений
о сути преобразований и возможностях библиотек OpenGL.
В примерах
MSDN можно найти способ введения реакций на нажатия клавиш. Используем клавиши
стрелок для смещения объекта в плоскости Z = const. Введите в функцию main декларацию
4 обработчиков:
auxKeyFunc(AUX_DOWN,
KeyDown);
auxKeyFunc(AUX_UP,
KeyUp);
auxKeyFunc(AUX_LEFT,
KeyLeft);
auxKeyFunc(AUX_RIGHT,
KeyRight);
Теперь по аналогии
с мышиными событиями создайте самостоятельно функции обработки и меняйте внутри
них те переменные, от которых зависит трансляция изображения. Например:
void
_stdcall KeyDown()
{
gdTransY
-=0.1; // Сдвигаем изображение вниз
}
void
_stdcall KeyUp()
{
gdTransY
+= 0.1; // Сдвигаем изображение вверх
}
void
_stdcall KeyLeft()
{
gdTransX
-=0.1; // Сдвигаем изображение влево
}
void
_stdcall KeyRight()
{
gdTransX
+=0.1; // Сдвигаем изображение вправо
}
При тестировании
результата обратите внимание на поведение изображения. Например, чем больше
сдвиг вправо, тем лучше видна левая боковая грань. Кажется, что совместно с
перемещением объекта он поворачивается. Но это не так. Эффект объясняется особенностями
перспективной проекции.
|
Использование
списков
С кубиком быстро
расправляется любой компьютер и видеокарта, а вот с более сложными объектами
могут возникнуть затруднения, бороться с которыми можно с помощью нескольких
приемов. Один из них — использование заранее откомпилированных списков команд
OpenGL. Для иллюстрации этого приема создайте отдельную глобальную функцию:
void
DrawScene()
{
//======
Создаем новый список команд OpenGL
glNewList(I,GL_COMPILE);
//======
Сюда поместите код, рисующий куб,
//======
начиная со строки
static
float v[8][3] =
//======
и заканчивая
for
(int j = 0; j < 4; j++)
glVertex3fv(v[id[i]
[j] ] ) ;
}
glEnd()
;
glEndList
() ;
}
Список рисующих
команд OpenGL ограничивается операторными скобками вида:
glNewList(I,
GL_COMPILE);
//======
Здесь располагаются команды OpenGL
glEndList
() ;
Первый параметр
glNewList (типа GLuint) идентифицирует список с тем, чтобы разработчик мог одновременно
использовать несколько списков и вызывать их в нужные моменты времени по номеру.
Вызов нашего (единственного) списка мы будем производить командой glCallList(l);.
Команды, расположенные между строками giNewList(l, GL_COMPILE); и glEndListQ;,
будут откомпилированы и сохранены в списке номер один. В функции перерисовки
их следует просто воспроизвести. Для этого замените существующую версию функции
OnDraw на новую:
void
_stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT
| GL_DEPTH_BUFFER_BIT);
glMatrixMode
(GL_MODELVIEW);
glLoadldentity
();
glTranslated(gdTransX,
gdTransY, gdTransZ);
glRotated(gdAngleY,
0.,1.,0.);
glRotated(gdAngleX,
1. ,0 .,0 .);
//======
Воспроизводим команды из списка 1
glCallList
(1);
auxSwapBuffers();
}
Вызов DrawScene
можно осуществить внутри функции initQ, то есть один раз за время существования
программного модуля:
void
Init ()
{
glClearColor
(1., 1., 1., 0.);
//======
Включаем интерполяцию цветов полигона
glShadeModel
(GL_SMOOTH);
glShadeModel
(GL_DEPTH_TEST) ;
glPolygonMode(GL_FRONT_AND_BACK,
GL_FILL),
glEnable(GL_LIGHTING);
glEnable(GL_LIGHTO);
glEnable(GL_COLOR_MATERIAL);
//======
Готовим сцену
DrawScene
() ;
}
|
Интерполяция
цвета
Вы можете запустить
новый вариант приложения и убедиться в том, что он работает не хуже предыдущего.
Кроме подготовки изображения (вызов DrawScene) мы внутри init включили возможность
интерполяции цвета точек при заполнении полигона
glShadeModel
(GL_SMOOTH) ;
Этого можно
было и не делать, так как она включена по умолчанию. Но мы вставили код, с тем
чтобы явно обозначить намерение исследовать эту возможность. Для того чтобы
интерполяция начала действовать, надо задать разные цвета разным вершинам одного
полигона. Уберите завершающий фрагмент функции DrawScene, где приведены коды
непосредственного рисования куба, и вставьте вместо него следующий текст:
glPolygonMode
(GL_FRONT_AND_BACK, GL_FILL) ;
glBegin
(GL_QUADS) ;
//======
Обновляем генератор случайных чисел
srand
(time (0) ) ;
//======
6 граней куба
for
(int i = 0; i < 6; i++) ( glNormalSdv (norm[i] ) ;
//======
4 вершины одной грани
for
(int j = 0; j < 4; j++)
{
//======
Задаем различные цвета
glColorSd
(rand()%10/10.,
rand()%10/10.,
rand()%10/10.) ;
glVertex3fv(v[id[i]
[ j ] ] ) ;
}
}
glEnd()
;
glEndList
() ;
Включите в
начало файла директиву препроцессора:
#include
<time.h>
для того чтобы
стала доступной функция timeQ. Она помогает настроить генератор псевдослучайных
чисел так, чтобы при разных запусках программы получать различные комбинации
цветов. Двойное деление на 10 (rand()%10/10.) позволяет масштабировать и нормировать
компоненты цвета. Запустите и проверьте качество интерполяции цветов.
|
Строим
икосаэдр
Для иллюстрации
работы с массивами вершин создадим более сложный объект — икосаэдр. Это такой
дссятистенный дом с острой пятиугольной крышей и таким же полом, но углы пола
смещены (повернуты) на л/5 относительно углов потолка.
Икосаэдр имеет
20 треугольных граней и 12 вершин (1 + 5 на потолке и 1 + 5 на полу). Благодаря
своей правильности он может быть задан с помощью всего лишь двух чисел, которые
лучше вычислить один раз и запомнить. Этими числами является косинус и синус
угла в три пятых окружности, то есть
static
double
//======
atan(l.) - это пи/4
angle
= 3. * atan(1.)/2.5, //====== 2 характерные точки
V
= cos(angle), W = sin(angle);
Этот код мы
вставим внутрь функции рисования, чтобы не плодить глобальные переменные и не
нарываться на конфликты имен. Вот новая версия функции DrawScene:
void
DrawScene() {
static double
//======
2 характерные точки
angle
= 3. * atan(l.)/2.5, V = cos(angle), W = sin(angle),
//===
20 граней икосаэдра, заданные индексами вершин
static
GLuint id[20][3] =
|
|
|
|
|
|
|
|
(0,1,
4), (8,1,10), (7,3,10), (6,10,1),
|
(0,4,
9), (8,10,3), (7,10,6), (9,11,0),
|
(9,4,
5), (5,8, 3), (7,6,11), (9,2,11),
|
(4,8,
5), (5,3, 2), (11,6,0), (9,5, 2),
|
(4,1,8),
(2,3,7), (0,6,1), (7,11,2)
//======
Начинаем формировать список команд
glNewList
(1,GL_COMPILE) ;
//======
Выбираем текущий цвет рисования
glColor3d
(1., 0.4, 1 . ) ;
glBegin
(GLJTRIANGLES) ;
for
( int i = 0; i < 20; i++)
{
//======
Грубый подход к вычислению нормалей
glNorma!3dv(v[id[i]
[0] ] ) ;
glVertex3dv(v[id[i]
[0] ] ) ;
glNorma!3dv(v[id[i]
[1] ] ) ;
glVertex3dv(v[id[i]
[1] ] ) ;
glNorma!3dv(v[id[i]
[2] ] ) ;
glVertex3dv(v[id[i]
[2] ] ) ;
}
glEnd()
;
//======
Конец списка команд
glEndList
();
}
Точное
вычисление нормалей
Проверьте результат
и обсудите качество. В данном варианте нормали в вершинах заданы так, как будто
изображаемой фигурой является сфера, а не икосаэдр. Это достаточно грубое приближение.
Если поверхность произвольного вида составлена из треугольников, то вектор нормали
к поверхности каждого из них можно вычислить точно, опираясь на данные о координатах
вершин треугольника. Из $ курса векторной алгебры вы, вероятно, помните, что
векторное произведение двух векторов а и b определяется как вектор п, перпендикулярный
к плоскости, в которой лежат исходные векторы. Величина его равна площади параллелограмма,
построенного на векторах а и b как на сторонах, а направление определяется так,
что векторы a, b и п образуют правую тройку. Последнее означает, что если представить
наблюдателя на конце вектора п, то он видит поворот вектора а к вектору b, совершаемый
по кратчайшему пути против часовой стрелки. На рис. 6.4. изображена нормаль
п (правая тройка) при различной ориентации перемножаемых векторов а и b.
Рис.
6.2. Ориентация вектора нормали
Если координаты
векторов а и b известны, то координаты нормали вычисляю по следующим формулам.
Длина вектора нормали п зависит от длин вектор сомножителей и величины угла
между ними:
Nx=AxBz-AzBy
Ny=AzBx-AxBz
Nz=AxBy-AyBx
Примечание
Можно потерять много времени
на осознание того факта, что не только правление нормали, но и ее модуль влияют
на величину освещенности (и та) вершины, так как сопровождающая документация
(Help) не содер; явных указаний на это. Отметьте также, что цвета вершин полигона
влияю цвета точек заполнения полигона, так как цвета вновь генерируемых то
интерполируются, то есть принимают промежуточные значения между з чениями
цвета вершин.
Чтобы нивелировать
зависимость цвета вершины от амплитуды нормали, обыч вектор нормали масштабируют
(или нормируют), то есть делают его длину р; ной единице, оставляя неизменным
направление. С учетом сказанного создал две вспомогательные функции. Первая
масштабирует, а вторая вычисляет н< маль к плоскости треугольника. Алгоритм
вычисления использует координа двух сторон, прилегающих к текущей вершине треугольника:
//====Нормирование
вектора нормали (или любого другого)
void
Scale(double v[3])
{
double
d = sqrt(v[0]*v[0]+v[l]*v[l]+v[2]*v[2]);
if
(d == 0.)
{
MessageBox(0,"Zero
length vector","Error",MB_OK);
return;
}
void
getNorm(double vl[3], double v2[3], double out[3])
{
//=====
Вычисляем координаты вектора нормали
//======
по формулам векторного произведения
out[0]
= vl[l]*v2[2] - vl[2]*v2[l];
out[l]
= vl[2]*v2(0] - vl[0]*v2[2] ;
out[2]
=vl[0]*v2[l] - vl[l]*v2[0];
Scale(out);
}
Замените функцию
DrawScene. В новом варианте мы аккуратно вычисляем и масштабируем нормали в
каждом из двадцати треугольников поверхности икосаэдра:
void
DrawScene()
{
static
double
angle
- 3. * atanfl.)/2.5, V = cos(angle), W = sin(angle),
v[12]
[3] = {
{-V,0.,W},
{V,0.,W}, {-V,0.,-W},
{V,0.,-W},
{0.,W,V}, {0.,W,-V},
{0.,-W,V},
{0. ,-W,-V}, {W,V, 0.},
{-W,V,0.},
{W,-V,0.}, {-W,-V,0.}
};
static
GLuint id[20][3] = {
(0,1,
4), {0,4, 9}, (9,4, 5), (4,8, 5}, (4,1,8),
(8,1,10),
(8,10,3), (5,8, 3), (5,3, 2), (2,3,7),
(7,3,10),
(7,10,6), (7,6,11), (11,6,0), (0,6,1),
(6,10,1),
(9,11,0), (9,2,11), (9,5, 2), (7,11,2) 1;
glNewList(l,GL_COMPILE);
glColorSd (1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for
(int i = 0; i < 20; i++)
{
double
dl[3], d2[3], norm[3];
for
( int j = 0; j < 3 ; j++)
{
dl[j]
=v[id[i][0]] [j] -v[id[i][l]J [j];
d2[j]
=v[id[i][l]] [j] -v[id[i][2J] [j];
}
//======
Вычисление и масштабирование нормали
getNorm(dl,
d2, norm);
glNormal3dv(norm);
glVertexSdv(v
[ id[i] [1]]);
glVertex3dv(v[id[i]
[1] ] glVertex3dv(v[id[i] [2] ]
glEnd()
;
}
glEndList
() ;
}
Примечание
Функцию нормировки всех
нормалей можно возложить на автомат OpenGL, если включить состояние GL_NORMALIZE,
но обычно это ведет к замедлению перерисовки и, как следствие, выполнения
приложения, если изображение достаточно сложное. В нашем случае оно просто,
и поэтому вы можете проверить действие настройки, если вставите вызов glEnable
(GL_NORMALIZE); в функцию Init (до вызова OrawScene) и временно выключите
вызов Scale(out); производимый в функции getNorm. Затем вернитесь к исходному
состоянию.
Как
создать сферу
Для того чтобы
из существующей заготовки — икосаэдра из двадцати граней — создать сферу, круглую,
блестящую и без изъянов, нужно осуществить предельный переход, как в матанализе,
бесконечно увеличивая число треугольников при бесконечном уменьшении их размеров.
В дискретном мире нет места предельным переходам, поэтому вместо бесконечного
деления надо ограничиться каким-то конечным числом и начать делить каждый из
двадцати треугольников икосаэдра на все более мелкие правильные треугольники.
Вычисление нормали при этом упрощается, так как при приближении к шару нормаль
в каждой вершине треугольника приближается к нормали поверхности шара. А последняя
равна нормированному вектору радиуса текущей точки. Алгоритм деления проиллюстрируем
рисунком (рис. 6.3).
Рис.
6.3. Деление треугольника икосаэдра
Треугольник
с вершинами VI, V2 и V3 разбивается на четыре треугольника: (V1,V12,V31), (V2,V23,V12),
(V3,V32,V23) и (V12.V23.V31). После этого промежуточные точки деления надо посадить
на поверхность шара, то есть изменить их координаты так, чтобы концы векторов
(V12, V23 и V31) дотянулись до поверхности шара. Для этого достаточно нормировать
векторы с помощью уже существующей процедуры Scale. Она впоследствии будет использована
как для масштабирования нормали, так и для нормировки координат вершин новых
треугольников. Но сейчас мы будем вычислять нормаль приближенно. Введем еще
две вспомогательные функции:
//===
Команды OpenGL для изображения одного треугольника
void
setTria(double *vl, double *v2, double *v3)
{
//======
Нормаль и вершина задаются одним вектором
glNormal3dv(vl);
glVertex3dv(vl);
glNormalSdv
(v2);
glVertex3dv(v2);
glNormal3dv(v3);
glVertex3dv(v3);
glEnd()
;
}
//======
Генерация внутренних треугольников
void
Split(double *vl, double *v2, double *v3)
{
//======
Промежуточные вершины
double
v!2[3], v23[3], v31[3);
for
(int l=0; l< 3; i++) {
//======
Можно не делить пополам,
//======
так как будем нормировать
v12[i]
= vl[i]+v2[i];
v23[i]
= v2[i]+v3[i];
v31
[i] = v3[i]+vl [i];
}
//======
Нормируем три новые вершины
Scale(v!2);
Scale(v23);
Scale(v31);
//====== и рисуем четыре треугольника
setTria(vl,
v!2, v31);
setTria
(v2, v23, v!2);
setTria(v3,
v31, v23);
setTria(v!2,v23,
v31);
}
Вставьте эти
глобальные функции в файл и дайте следующую версию функцию DrawScene, в которой
отсутствует вызов функции getNorm для точного вычисления нормали, но есть вызов
функции Split для каждой из 20 граней икосаэдра. В результате мы получаем фигуру
из 80 треугольных граней, которая значительно ближе к сфере, чем икосаэдр:
void
DrawScene()
{
static
double
angle
= 3. * atan(l.)/2.5, V = cos (angle), W = sin (angle),
v[12]
[3] =
{-V,0.,W},
{V,0.,W}, {-V,.0.,-W},
(V,0.,-W),
{0.,W,V}, {0.,W,-V},
(0.,-W,V),
(0.,-W,-V), {W,V,0.},
{-W,V,0.},
{W,-V,0.}, {-W,-V,0.}
};
static
GLuint id[20][3] =
{
(0,1,
4), (0,4, 9), {9,4, 5}, (4,8, 5), (4,1,8),
(8,1,10),
(8,10,3), (5,8, 3), (5,3, 2), (2,3,7),
(7,3,10),
(7,10,6), (7,6,11), (11,6,0), (0,6,1),
(6,10,1),
(9,11,0), (9,2,11), (9,5, 2), (7,11,2)
};
glNewList(l,GL_COMPILE);
glColor3d
(1., 0.4, 1.) ;
glBegin(GLJTRIANGLES);
for
(int i = 0; i < 20; i++)
Split
(v[id[i][0]], v[id[i][l]], v[id[i] [2] ]) ;
glEnd()
;
glEndList
() ;
}
На этой стадии
я рекомендую посмотреть, какие интересные и неожиданные результаты могут быть
получены вследствие ошибок. Все мы ошибаемся, вот и я так долго возился с направлением
обхода и со знаком нормали, что в промежуточных вариантах получал чудовищные
комбинации. Многие из них «канули в Лету», но один любопытный вариант легко
смоделировать. Если ошибки происходят в условиях симметричного отражения, то
возникают ситуации, сходные со случайными изменениями узоров в калейдоскопе.
Замените на обратные знаки компонентов вектора в функции Scale. Это действие
в предыдущих версиях программы было эквивалентно изменению знака нормали. Найдите
строку, похожую на ту, что приведена ниже, и замените знаки так, как показано,
на минусы.
v[0]
/= -d; v[l] /= -d; v[2] /= -d;
|
Выбор
способа вычисления нормалей
Верните знаки
на место и продолжим процесс приближения к шару. Прежде всего восстановим возможность
точного вычисления нормалей и дадим пользователю возможность интерактивного
выбора между приближенным и точным их вычислением. С этой целью введем глобальную
переменную
//======
Флаг способа вычисления нормалей
bool
gbSmooth = false;
которая будет
помнить текущий способ вычисления нормалей, и сделаем так, чтобы каждое нажатие
клавиши N инвертировало эту переменную и способ вычисления нормали. Введите
в функцию main реакцию на нажатие клавиши N, вставив строку
auxKeyFunc(AUX_n,
KeyN);
Реализацию
функции обработки вставьте до функции main:
void
_stdcall KeyN()
{
//======
Изменяем способ вычисления нормалей
gbSmooth
= !gbSmooth;
11======
Заново создаем список команд
DrawScene();
}
Введите новую
версию функции setTria, которая учитывает выбранный способ вычисления нормалей:
void
setTria(double
*vl, double *v2, double *v3)
{
glBegin(GLJTRIANGLES);
//======
Если выбран способ точного вычисления нормали
if
(!gbSmooth)
{
//======
Правая тройка векторов
double
dl[3], d2[3], norm[3];
//======
Вычисляем координаты векторов
//======
двух сторон треугольника
for
(int j = 0; j.< 3; j++)
{
dl[j]
= vl[j] - v2[j); d2[j] = v2[j] - v3[j];
}
//======
Вычисляем нормаль к плоскости
//======
треугольника со сторонами dl и d2
getNorm(dl,
d2, norm);
glNormalSdv(norm);
glVertex3dv(vl);
glVertex3dv(v2);
glVertex3dv(v3);
}
else
{
//===
Неточное (приближенное) задание нормали
glNorma!3dv(vl);
glVertexSdv(vl);
glNorma!3dv(v2);
glVertex3dv(v2);
glNorraalSdv(v3);
glVertex-3dv(v3);
}
glEnd
();
}
Запустите и
проверьте результат, нажимая клавишу N. Надеюсь, что теперь важность точного
вычисления нормалей стала для вас еще более очевидной.
|
Рекурсивное
деление
Добавим возможность
дальнейшего деления треугольников и образования фигур, приближающихся к сфере.
Осуществление предельного перехода возможно в математике непрерывных величин,
но невозможно в дискретной математике и тем более в реализациях ее алгоритмов,
где все множества должны быть конечными. Однако мы можем применить конечную
рекурсию для дальнейшего деления треугольников. Замените функцию split на ее
рекурсивную версию. Вы, конечно, помните, что рекурсивная функция вызывает сама
себя до тех пор, пока не выполнится некоторое условие. Здесь условием выхода
из цепи рекурсивных вызовов является равенство нулю последнего параметра depth,
который определяет текущую глубину рекурсии:
void
Split(double *vl,
double *v2, double *v3,long depth)
{
double
v12[3], v23[3], v31[3];
if
(depth == 0)
{
//======
Рисование наименьших треугольников
setTria(vl,
v2, v3);
//======
и выход из цепи рекурсивных вызовов
return;
}
//======
Разбиение треугольника
for
(int i = 0; i < 3; i++)
{
v12[i]
= vl[i]+v2[i];
v23[i]
= v2[i]+v3[ij;
v31[i]
= v3[i]+vl[i];
}
//======
Дотягивание до сферы
Scale(v12);
Scale(v23);
Scale(v31);
//====== Рекурсивное разбиение на
//======
четыре внутренних треугольника
Split(vl,
v!2, v31, depth-1);
Split(v2,
v23, v12, depth-1);
Split(v3,
v31, v23, depth-1);
Split(v!2,
v23, v31, depth-1);
}
Внесите также
изменение в ту строку программы, где происходит вызов Split. Надо добавить параметр,
задающий глубину рекурсии. Если функцию вызвать с нулевой глубиной, то получим
икосаэдр, если увеличивать глубину, то будем получать фигуры, более близкие
к шару:
for
(int i = 0; i < 20; i++)
Split
(v[id[i) [0]], v[id[i][l]], v[id[i] [2]], 3);
Запустите и
проверьте, нажимая клавишу N. Попробуйте изменить глубину рекурсии, только не
переусердствуйте. Если задать глубину более 10, то можно не дождаться ответа.
Рекурсия дорого стоит, поэтому исследованный подход абсолютно неприемлем для
создания сферы. Аналогичный вывод справедлив для других объемных изображений,
создаваемых с помощью задания вершин большого числа геометрических примитивов.
В данный момент
для иллюстрации процесса приближения изображаемой фигуры к сфере напрашивается
такой сценарий: пользователь нажимает клавишу — пробел, глубина рекурсии изменяется
и изображение пересчитывается. Алгоритм управления глубиной рекурсии, очевидно,
следует выбрать таким, чтобы, оставаясь в рамках допустимых значений, можно
было проходить весь диапазон в обе стороны. Введите в функцию main обработку
нажатия клавиши пробела:
auxKeyFunc(AOX_SPACE,
KeySpace);
и
создайте функцию обработки:
void
_stdcall KeySpace()
{
//======
Флаг роста числа разбиений
static
bool bGrow = true;
//======
Продолжаем разбивать до глубины 4
if
(bGrow SS giDepth < 4)
{
giDepth
+= 1;
}
//======
Смена знака при глубине 4
else
if (giDepth > 0)
{
bGrow
= false;
giDepth
== 1;
}
//======
Смена знака при глубине О
else
{
bGrow
= true;
giDepth
+= 1;
}
DrawScene
() ;
}
Алгоритм предполагает,
что глобально определена переменная giDepth, которая хранит текущее значение
глубины рекурсии. Добавьте к существующим глобальным переменным объявление:
//======
Глубина рекурсии
int
giDepth;
В функции DrawScene
замените параметр 3 (при вызове Split) на giDepth и запустите на выполнение.
Примечание
Не знаю, как объяснить,
но в Visual Studio б этот код почему-то работает, не-— смотря на явный промах,
который типичен не только для начинающих программистов. Опытный читатель,
конечно же, заметил, что мы создаем новые списки изображений, не уничтожая
старые. Такие действия классифицируются как утечка памяти (memory lickage).
Для ее устранения вставьте следующий фрагмент в функцию DrawScene перед вызовом
glNewList:
//======
Если существует 1-й список,
if
(gllsList(1))
//======
то освобождаем память
glDeleteLists
(1,1);
Разъяснения
можно найти в справке по функциям gllsList и glDeleteLists. He ошибитесь при
выборе места вставки фрагмента, так как операции с памятью имеют особую важность.
Запустите приложение и, нажимая на пробел, наблюдайте за изменением изображения,
которое сначала приближается к сфере, затем постепенно возвращает свой первоначальный
облик икосаэдра. Периодически нажимайте клавишу N для того, чтобы оценить влияние
точного вычисления нормалей.
|
Массивы вершин, нормалей и цветов
Неэффективность
алгоритма последовательного рисования большого числа примитивов не является
тайной для тех, кто имеет дело с трехмерной графикой. Поэтому в технологии OpenGL
существует ряд приемов (поддержанных функциями), которые основаны на использовании
заранее заготовленных массивов, а также списков команд OpenGL. Они значительно
повышают эффективность работы конвейера при передаче (rendering) изображений,
составленных из десятков и сотен тысяч примитивов. Например, функция glDrawElements
позволяет задать геометрические примитивы экономичным способом, то есть с минимальными
затратами на вызовы функций. До сих пор мы вызывали в среднем 4-5 функций для
каждого треугольника. При этом многократно повторялись, так как вершины, общие
для смежных треугольников, задавались не один раз. Массивы величин, ассоциируемых
с вершинами (координаты, нормали, цвета и другие), могут быть сформированы заранее
и использованы при описании геометрии с помощью массива индексов. Функция glDrawElements
требует в качестве одного из параметров массив индексов вершин полигонов. Вот
ее прототип:
void
glDrawElements
(GLenum mode, GLsizei count,
GLenum
type, const GLvoid *indices);
Функция конструирует
count элементов типа mode. Параметр indices должен содержать адрес массива индексов,
который формируется заранее. Параметр type определяет тип элементов массива
indices. Он может принимать одно из трех фиксированных значений: GL_UNSIGNED_BYTE
(используется 8-битовый индекс), GL_UNSIGNED_SHORT (16-биТНЫЙ ИНДСКС), GL_UNSIGNED_INT
(32-биТНЫЙ). Характерной особенностью рассматриваемой технологии является то,
что величины, ассоциируемые с каждой вершиной примитива, могут храниться в разных
массивах или в одном массиве структур с разными полями. Они задаются с помощью
6 функций:
GIVertexPointer — задает
адрес массива координат вершин;
GINormalPointer — задает
адрес массива нормалей в вершинах;
GlColorPointer — задает
адрес массива цветов, связанных с вершинами;
GlTexCoordPointer — задает
адрес массива координат текстуры материала, задаваемой в вершинах;
GlEdgeFlagPointer —
задает адрес массива флагов видимости линий, исходящих из вершины;
GllndexPointer — задает
адрес массива цветовых индексов вершин в режиме цветовой палитры, а не RGBA.
Другой массив
индексов — indices, определяет порядок выбора элементов из этих шести массивов.
Но этого мало — надо произвести еще некоторые настройки в машине состояний OpenGL.
Для перевода ее в режим использования массивов надо несколько раз вызвать функцию
glEnableClientstate. Каждый вызов включает один из шести рассмотренных режимов.
Только после этого функция glDrawElements способна эффективно задать сразу все
примитивы. Например, вызов:
glEnableClientState(GL_VERTEX_ARRAY);
включает режим
использования массива координат вершин, а вызов этой же функции с параметром
GL_NORMAL_ARRAY включает использование массива нормалей.
Совместно с
командой glDrawElements обычно используют тот способ повышения эффективности
отображения примитивов, который мы уже используем. Речь идет о паре функций:
glNewList, glEndList. Все команды OpenGL, заданные между вызовами этих двух
функций, оптимизируются, компилируются (по выбору) и запоминаются в отдельном
нумеруемом списке.
|
Создание
сферы
Для иллюстрации
рассматриваемых возможностей мы создадим сферу, составленную из треугольников,
но при этом не будем отталкиваться от какого-либо правильного многогранника,
а используем модель глобуса. Количество и пропорции треугольников будут зависеть
от количества геодезических линий на сфере (параллелей и меридианов). Если вы
посмотрите на рис. 6.4 или представите себе глобус, то согласитесь с тем, что
параллели и меридианы разбивают поверхность сферы на множество сферических четырехугольников.
Исключение составляют лишь полюса, вокруг которых мы имеем сферические треугольники.
Если затем каждый сферический четырехугольник разделить диагональю, то он даст
два сферических треугольника.
Рис. 6.4.
Разбиение сферы на треугольники
Мы будем управлять
степенью дискретизации сферы с помощью двух чисел: количества колец (gnRings)
и количества секций (gnSects). Они определяют как полное количество вершин,
так и треугольников. Если глобально зададим переменные:
const
UINT gnRings =
20; // Количество колец (широта)
const
UINT gnSects = 20; // Количество секций (долгота),то, так как каждый прямоугольник
разбит на два треугольника, общее количество треугольников будет:
const
UINT gnTria = (gnRings+1) * gnSects * 2;
//===Нетрудно
подсчитать и общее количество вершин:
const
UINT gnVert = (gnRings+1) * gnSects + 2;
Мы уже, по
сути, начали писать код, поэтому создайте новый файл Sphere.срр и подключите
его к проекту, а предыдущий файл OG.cpp отключите. Эти действия производятся
так:
Поставьте фокус на элемент
дерева OG.cpp в окне Solution Explorer и нажмите клавишу Delete. При этом
файл будет отключен от проекта, но он останется в папке проекта.
Переведите фокус на строку
Console того же окна и, вызвав контекстное меню, дайте команду Add New Item.
Выберите шаблон C++ File
(.срр) и, задав имя файла Sphere.срр, нажмите ОК.
Введите в него
директивы препроцессора, которые нам понадобятся, а также объявления некоторых
констант:
#include
<stdlib.h>
#include
<stdio.h>
#include
<math.h>
#include
<string.h>
#include
<time.h>
#include
<windows.h>
#include
<gl\gl.h>
#include
<gl\glu.h>
#include
<gl\glaux.h>
const
UINT gnRings = 40; // Количество колец (широта)
const
UINT gnSects = 40; // Количество секций (долгота)
//======
Общее количество треугольников
const
UINT gnTria = (gnRings+1) * gnSects * 2;
//======
Общее количество вершин
const
UINT gnVert = (gnRings+1) * gnSects + 2;
//======
Два цвета вершин
const
COLORREF gClrl = RGB(0, 255, 0);
const
COLORREF gClr2 = RGB(0, 0, 255);
const
double gRad = 1.5; // Радиус сферы
const
double gMax =5.; // Амплитуда сдвига
const
double PI = atan(1.)*4,; // Число пи
Класс
точки в 3D
С каждой вершиной,
как вы помните, связано множество параметров, определяющих качество изображения
OpenGL. Мы остановимся на наборе из трех величин: координаты вершины, вектор
нормали и цвет. Так как вектор нормали и координаты можно задать с помощью двух
объектов одного и того же типа (три вещественных переменных х, у, z), то целесообразно
ввести в рассмотрение такое понятие, как точка трехмерного пространства. И воплотить
его в виде класса CPoint3D, который инкапсулирует функциональность такой точки.
Введите определение класса в конец файла Sphere. срр:
//======
Точка 3D-пространства
class
CPointSD
{
public:
float x, у, z; // Координаты точки
//
====== Конструктор по умолчанию
CPoint3D
() { х = у = z = 0; ) //====== Конструктор с параметрами
CPointSD
(double cl, double c2, float c3)
{
x
= float (cl) ;
z
= float(c2) ;
у
= float(c3) ;
}
//======
Операция присвоения
CPoint3D&
operator= (const CPoint3D& pt)
{
x
= pt.x;
z
= pt . z ;
У
= Pt.y;
return
*this;
//======
Операция сдвига в пространстве
CPoint3D&
operator+= (const CPoint3D& pt)
{
x
+= pt.x;
y
+= Pt.y;
z
+= pt . z ;
return
* this ;
}
//======
Конструктор копирования
CPointSD
(const CPoint3D& pt)
{
*this
= pt;
}
};
Обратите внимание
на тот факт, что конструктор копирования использует код уже существующей операции
присвоения. Имея в своем распоряжении класс CPointSD, мы можем создать еще один
тип данных — структуру, поля которой объединяют все величины, связанные с вершиной
треугольника. Массив данных такого типа будет хранить информацию обо всех вершинах
изображения и при этом не будет повторений:
//======
Данные о вершине геометрического примитива
struct
VERT
{
CPointSD
v; // Координаты вершины
CPoiivt3D
n; // Координаты нормали
DWORD
с; // Цвет вершины
};
Введите эту
декларацию после кода, определяющего CPoint3D. Как было отмечено, функция glDrawElements
в качестве параметра требует задать массив индексов вершин. В соответствии с
этими индексами вершины треугольников будут выбираться из общего массива вершин.
Порядок следования индексов зависит от порядка обхода вершин при задании треугольников.
Как вы помните, он должен идти против часовой стрелки, если смотреть на примитив
с конца внешней нормали. В этом случае знак нормали соответствует формулам векторной
алгебры,!: которые мы уже рассматривали.
Будет удобно,
если мы сначала создадим структуру, которая объединяет три индекса вершин одного
треугольника. Тогда массив структур такого типа сможет играть роль массива индексов,
требуемого функцией glDrawElements. Введите следующее описание в продолжение
файла:
struct
TRIA
{
//======
Индексы трех вершин треугольника,
//======
выбираемых из массива вершин типа VERT
//======
Порядок обхода — против часовой стрелки
int
i1;
int
i2;
int
i3;
};
Далее нам понадобятся
две глобальные неременные типа CPointSD, с помощью *":' которых
мы будем производить анимацию изображения сферы. Анимация, а также различие
цветов при задании вершин треугольников позволят более четко передать трехмерный
характер изображения. Наличие освещения подвижного объекта также заметно увеличивает
его реалистичность. При создании програм-| мы мы обойдемся одним файлом, поэтому
новые объявления продолжайте вставлять в конец файла Sphere.срр:
//======
Вектор углов вращения вокруг трех осей ?
CPointSD
gSpin; //====== Вектор случайной девиации вектора gSpin
CPointSD
gShift;
При каждой
смене буферов (перерисовке изображения) мы будем вращать изоб- ; ражение сферы
вокруг всех трех осей на некоторый векторный квант gshif t. Для того чтобы вращение
было менее однообразным, введем элемент случайности. Функция Rand, приведенная
ниже, возвращает псевдослучайное число в диапазоне (-х, х). Мы будем пользоваться
этим числом при вычислении компонентов вектора gshif t. Последний, воздействуя
на другой вектор gSpin, определяет новые значения трех углов вращения, которые
функция glRotate использует для задания очередной позиции сферы:
inline
double Rand(double x)
{
//======
Случайное число в диапазоне (-х, х)
return
х - (х + х) * rand() / RAND_MAX;
}
Учитывая
сказанное, можно создать алгоритм перерисовки:
void
_stdcall OnDraw()
{
glClear(GL_COLOR_BUFFER_BIT)
;
//===
Сейчас текущей является матрица моделирования
glLoadldentityО;
//======
Учет вращения
glRotated(gSpin.х,
1., О, 0.) ;
glRotated(gSpin.y,
0., 1., 0.);
glRotated(gSpin.z,
0., 0., 1.) ;
//======
Вызов списка рисующих команд
glCallList(1);
//======
Подготовка следующей позиции сферы
gSpin
+= gShift;
//=====
Смена буферов auxSwapBuffers();
}
Подготовка
сцены
Изображение
сферы целесообразно создать заранее (в функции init), а затем воздействовать
на него матрицей моделирования, коэффициенты которой изменяются в соответствии
с алгоритмом случайных девиаций вектора вращения. При разработке кода функции
init надо учесть специфику работы с функцией glDrawElements, которая обсуждалась
выше. Кроме того, здесь мы производим установку освещенности, технологию и детали
которой можно выяснить в сопровождающей документации (MSDN). Введите следующие
коды функции инициализации и вставьте их до функции перерисовки:
void
Init ()
{
//===
Цвет фона (на сей раз традиционно черный)
glClearColor
(0., 0., 0., 0.);
//======
Включаемаем необходимость учета света
glEnable(GL_LIGHTING);
//===
Включаемаем первый и единственный источник света
glEnable(GL_LIGHT());
//======
Включаем учет цвета материала объекта
glEnable(GL_COLOR_MATERIAL);
//
Вектор для задания различных параметров освещенности
float
v[4] =
{
0.0Sf,
0.0Sf, 0.0Sf, l.f
};
//===
Сначала задаем величину окружающей освещенности glLightModelfv(GL_LIGHT_MODEL_AMBIENT,
v);
//======
Изменяем вектор
v[0]
= 0.9f; v[l] = 0.9f; v[2] = 0.9f;
//======
Задаем величину диффузной освещенности
glLightfv(GL_LIGHTO,
GL_DIFFUSE, v) ;
//=======
Изменяем вектор
v[0]
= 0.6f; v[l] = 0.6f; v[2] = 0.6f;
//======
Задаем отражающие свойства материчала
glMaterialfv(GL_FRONT,
GL_SPECULAR, v);
//======
Задаем степень блесткости материала
glMateriali(GL_FRONT,
GL_SHININESS, 40);
//======
Изменяем вектор
v[0]
= O.f; v[l] = O.f; v[2] = l.f; v[3] = O.f;
//======
Задаем позицию источника света
glLightfv(GL_LIGHTO,
GL_POSITION, v);
//======
Переключаемся на матрицу проекции
glMatrixMode(GL_PROJECTION);
glLoadldentity();
//======
Задаем тип проекции
gluPerspective(45,
1, .01, 15);
//===
Сдвигаем точку наблюдения, отодвигаясь от
//===
центра сцены в направлении оси z на 8 единиц
gluLookAt
(0, 0, 8, 0, 0, 0, 0, 1, 0) ;
//======
Переключаемся на матрицу моделирования
glMatrixMode(GL_MODELVIEW);
//=====
Включаем механизм учета ориентации полигонов
glEnable(GL_CULL_FACE);
//=====
Не учитываем обратные поверхности полигонов
glCullFace(GL_BACK);
//======
Настройка OpenGL на использование массивов
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
//======
Захват памяти под динамические массивы
VERT
*Vert = new VERT[gnVert];
TRIA
*Tria = new TRIA[gnTria];
//======
Создание изображения
Sphere(Vert,
Trial;
//======
Задание адресов трех массивов (вершин,
//======
нормалей и цветов),
/1======
а также шага перемещения по ним
glVertexPointer(3,
GL_FLOAT, sizeof(VERT), &Vert->v); glNormalPointer(GL_FLOAT, sizeof(VERT),
&Vert->n);
glColorPointer(3,
GL_UNSIGNED_BYTE, sizeof(VERT),
SVert->c);
srand(time(0));
// Подготовка ГСЧ
gShift
= CPoint3D (Rand(gMax),Rand(gMax),Rand(gMax));
//======
Формирование списка рисующих команд
glNewListd,
GL_COMPILE);
glDrawElements(GL_TRIANGLES,
gnTria*3, GL_UNSIGNED_INT, Tria);
glEndList()
;
//==
Освобождение памяти, так как список сформирован
delete
[] Vert;
delete
[] Tria;
}
Формула
учета освещенности
Семейство функций
glLightModel* позволяет установить общие параметры освещенности сцены. В частности,
первый параметр GL_LIGHT_MODEL_AMBIENT сообщает OpenGL, что второй параметр
содержит четыре компонента, задающие RGBA-интенсивность освещенности всей сцены.
По умолчанию вектор освещенности сцены равен (0.2, 0.2, 0.2, 1.0). Команда glLight*
устанавливает параметры источника света. Мы пользуемся ею два раза для задания
диффузного и рефлективного компонента интенсивности света. Если вы обратитесь
к документации, то увидите, что с помощью glLight* можно задать еще более десятка
параметров источника света. Формулу учета освещения я нашел в документации лишь
в словесном описании, но рискну привести ее в виде математического выражения.
В режиме RGBA-интенсивность
каждого из трех компонентов цвета освещенной вершины вычисляется как сумма нескольких
составляющих. Первая составляющая учитывает эмиссию света материалом, вторая
— освещенность окружения (ambient) или всей сцены, третья — является суммой
вкладов от всех источников света. Максимально допустимое число источников, как
вы помните, определено константой GL_MAX_LIGHTS, которая в нашем случае равна
8:
L=Me+MaLaf+Сумма(MaLai+MdLdi(N*Vl)+MsLsi(Ve*Vl)^h)
Здесь символ
т обозначает некоторое свойство материала, а символ / — свойство света.
Индекс е в применении к материалу обозначает эмиссию, а в применении
к
вектору v
— eye (глаз). Остальные индексы в применении к материалу обозначают различные
компоненты его отражающих свойств.
Mа — коэффициент
отражения окружающего (ambient) света,
Md — коэффициент
отражения рассеянного (diffuse) отражения,
Ms — коэффициент
отражения зеркального (specular) отражения,
N— вектор нормали вершины,
который задан командой glNormal,
V1—
нормированный вектор, направленный от вершины к источнику света,
Ve — нормированный
вектор, направленный от вершины к глазу наблюдателя,
h — блесткость
(shininess) материала.
Члены в круглых
скобках — это скалярные произведения векторов. Если они дают отрицательные значения,
то конвейер заменяет их нулем. Alpha-компонент результирующего цвета освещения
устанавливается равным alpha-компоненту диффузного отражения материала. Так
как мы задали лишь один источник света (LIGHTO), то знак суммы можно опустить.
Обратите внимание на то, что блесткость материала уменьшает (обостряет) пятно
отраженного света, так как возведение в степень h > 1 чисел (v, -v,), меньших
единицы, уменьшает их значение. Параллельные векторы v, и v, дадут максимальный
вклад. Чем больше их рассогласование, тем меньший вклад даст последний член
формулы.
Ориентация
поверхности
Кроме установки
параметров света код функции init содержит довольно много других установок,
которые мы осуществляем впервые, поэтому обсудим их более подробно. Возможно,
вы помните из курса аналитической геометрии, что некоторые поверхности имеет
ориентацию. По умолчанию поверхность любого полигона считается лицевой (FRONT),
если вы задали ее обходом вершин против часовой стрелки, и она считается изнаночной
(BACK), если направление обхода было обратным. В частности, ориентация поверхности
влияет на ориентацию нормали.
Примечание
Вы можете реверсировать
эту установку, задав режим glfrontFace (GL_CW). По умолчанию действует установка
glFrontFace(GL_CCW). Аббревиатура CW означает clockwise (по часовой стрелке),
a CCW — counterclockwise (против часовой стрелки). Кстати, вы, вероятно, видели
в литературе изображение ленты Мебиуса или бутылки Клейна, поверхности которых
односторонние и поэтому не имеют ориентации.
Команда glEnable
(GL_CULL_FACE); включает механизм учета ориентации поверхности полигонов. Она
должна сопровождаться одним из флагов, определяющих сторону поверхности, например
glCullFace(GL_BACK);. Таким образом, мы сообщаем конвейеру OpenGL, что обратные
стороны полигонов можно не учитывать. В этом случае рисование полигонов ускоряется.
Мы не собираемся показывать внутреннюю поверхность замкнутой сферы, поэтому
эти установки нам вполне подходят.
Массив
вершин, нормалей и цветов
Три команды
glEnableClientstate говорят о том, что при формировании изображения будут заданы
три массива (вершин, нормалей и цветов), а три команды вида gl* Pointer непосредственно
задают адреса этих массивов. Здесь важно правильно задать не только адреса трех
массивов, но и шаги перемещения по ним. Так как мы вместо трех массивов пользуемся
одним массивом структур из трех полей, то шаг перемещения по всем трем компонентам
одинаков и равен sizeof (VERT) — размеру одной переменной типа VERT. Массив
вершин (vert типа VERT*) и индексов их обхода (Tria типа TRIA*) создается динамически
внутри функции init. Характерно, что после того, как закончилось формирование
списка рисующих команд OpenGL, мы можем освободить память, занимаемую массивами,
так как вся необходимая информация уже хранится в списке. Формирование массивов
производится в функции Sphere, которую еще предстоит разработать.
Далее по коду
Init идет формирование списка рисующих команд. Так как массивы вершин и индексов
их обхода при задании треугольников уже сформированы, то список рисующих команд
создается с помощью одной команды glDrawElements. Ее параметры указывают:
тип геометрических примитивов
(GL_TRIANGLES);
размер массива индексов,
описывающих порядок выбора вершин (gnTria*3);
тип переменных, из которых
составлен массив индексов (GL_UNSIGNED_INT);
адрес начала массива
индексов.
Команды:
srandftime(0));
// Подготовка ГСЧ
gShift
= CPoint3D(Rand(gMax), Rand(gMax), Rand(gMax));
позволяют задать
характер вращения сферы. Константа const double gMax = 5.;
выполняет роль
регулятора (ограничителя) степени подвижности сферы. Если вам захочется, чтобы
сфера вращалась более резво, то увеличьте эту константу и перекомпилируйте проект.
Формирование
массива вершин и индексов
Самой сложной
задачей является правильное вычисление координат всех вершин треугольников и
формирование массива индексов Tria, с помощью которого команда glDrawElements
обходит массив Vert при задании треугольников. Функция Sphere реализует алгоритм
последовательного обхода сначала всех сферических треугольников вокруг полюсов
сферы, а затем обхода сферических четырехугольников, образованных пересечением
параллелей и меридианов. В процессе обхода формируется массив вершин vert. После
этого обходы повторяются для того, чтобы заполнить массив индексов Tria. Северный
и южный полюса обрабатываются индивидуально. Для осуществления обхода предварительно
создаются константы:
da — шаг изменения сферического
угла а (широта),
db — шаг изменения сферического
угла b (долгота),
af и bf — конечные значения
углов.
Для упрощения
восприятия алгоритма следует учитывать следующие особенности, связанные с порядком
обхода вершин:
После обработки северного
и южного полюсов мы движемся вдоль первой широты (a=da) от востока к западу
по невидимой части полусферы и возвращаемся назад по видимой ее части. Затем
происходит переход на следующую широту (а += da) и цикл повторяется.
Координаты вершин (х,
z) представляют собой проекции точек на экваториальную плоскость, а координата
у постоянна для каждой широты.
При обработке одной секции
кольца для двух треугольников формируется по три индекса:
void
Sphere(VERT *v,
TRIA* t)
{
//======
Формирование массива вершин
//======
Северный полюс
v[0].v
= CPointSD (0, gRad, 0);
v[0].n
= CPoint3D (0, 1, 0);
v[0].с
= gClr2;
//======
Индекс последней вершины (на южном полюсе)
UINT
last = gnVert - 1; //====== Южный полюс
v[last].v
= CPointSD (0, -gRad, 0);
v[last].n
= CPointSD (0, -1, 0) ;
v[last].c
= gnVert & 1 ? gClr2 : gClrl;
//======
Подготовка констант
double
da = PI / (gnRings +2.),
db
= 2. * PI / gnSects,
af
= PI - da/2.;
bf
= 2. * PI - db/2.;
//===
Индекс вершины, следующей за северным полюсом
UINT
n = 1;
//===
Цикл по широтам
for
(double a = da; a < af; a += da)
{
//===
Координата у постоянна для всего кольца
double
у = gRad * cos(a),
//======
Вспомогательная точка
xz
= gRad * sin(a);
//======
Цикл по секциям (долгота)
for
(double b = 0.; b < bf; n++, b += db)
}
//
Координаты проекции в экваториальной плоскости
double
х = xz * sin(b), z = xz * cos(b);
//======
Вершина, нормаль и цвет
v[n].v
= CPointSD (x, у, z);
v[n].n
= CPointSD (x / gRad, у / gRad, z / gRad);
v[n].c
= n & 1 ? gClrl : gClr2; } }
//======
Формирование массива индексов
//======
Треугольники вблизи полюсов
for
(n = 0; n < gnSects; n++)
{
//======
Индекс общей вершины (северный полюс)
t[n]
.11 = 0;
//======
Индекс текущей вершины
t[n]
.12 = n + 1;
//======
Замыкание
t[n].13
= n == gnSects - 1 ? 1 : n + 2;
//======
Индекс общей вершины (южный полюс)
t
[gnTria-gnSects+n] .11 = gnVert - 1;
t
tgnTria-gnSects+n] . 12 = gnVert - 2 - n;
t
[gnTria-gnSects+n] .13 = gnVert - 2
t
( (1 + n) % gnSects) ;
}
//======
Треугольники разбиения колец
//======
Вершина, следующая за полюсом
int
k = 1;
//======
gnSects - номер следующего треугольника
S'
n = gnSects;
for
(UINT i = 0; i < gnRings; i++, k += gnSects) {
for
(UINT j = 0; j < gnSects; j++, n += 2) {
//=======
Индекс общей вершины
t[n]
.11 = k + j;
//=======
Индекс текущей вершины
t[n].12
= k + gnSects + j;
//=======
Замыкание
t[n].13
= k + gnSects + ((j + 1) % gnSects)
//=======
To же для второго треугольника
t[n
+ 1].11 = t[n].11;
t[n
+ 1].12 = t[n].13;
t[n
+ 1].13 = k + ((j + 1) % gnSects);
Для завершения
работы осталось дополнить программу стандартным набором процедур, алгоритм функционирования
которых вы уже изучили:
void_stdcall
OnSize(GLsizei
w, GLsizei h) { glViewport(0, 0, w, h);
}
void
main ()
{
auxInitDisplayMode(AUX_RGB
| AUX_DOUBLE) ;
auxInitPositiondO,
10, 512, 512);
auxInitwindow("Vertex
Array");
Init()
;
auxReshapeFunc
(OnSize) ;
auxIdleFunc
(OnDraw) ;
auxMainLoop
(OnDraw) ;
}
Запустите проект
на выполнение и уберите возможные неполадки. Исследуйте функционирование программы,
вводя различные значения глобальных параметров (регулировок). Попробуйте задать
нечетное число секций. Объясните результат. В качестве упражнения введите
возможность интерактивного управления степенью дискретизации сферы и исследуйте
эффективность работы конвейера при ее увеличении.
|
| | |