OpenGL в Delphi - Информация книги

Информация книги

Книга подготовлена для чтения и переведена в формат CHM сайтом Дельфи и Компания. Это было сделано собственно потому, что читать такой файл намного удобнее чем выкачивать главы отдельно в сети, при этом еще попутно получая баннеры, менюшки и прочую ненужную ерунду. :)
Примеры к книге на Delphi вы можете перекачать на нашем сайте.

Автор - Михаил Краснов.

У нас вы можете найти Русскую справку по Windows API для Delphi, которая распространяется бесплатно. Также возможно подписаться на рассылку новостей прямо на сайте или на странице Subscribe.ru.
На сайте можно получить дополнительные материалы по программированию. Например книги, советы, FAQ. Правда пока еще не очень много, но мы постараемся пополнять коллекцию матералов.
Можно зайти в раздел статьи.
Будем рады видеть вас на нашем сайте. Оставляйте свои пожелания и замечания в гостевой книге.

PS: Судя по всему, эту книгу сканировали, затем перевели в формат HTML, из-за чего в тексте громадное количество ошибок. Огромная просьба к вам, если вы заметили ошибку, то пишите мне и я ее исправлю.

Приятного чтения и удачи вам в программировании." :)


OpenGL в Delphi

Содержание

Введение
Глава 1. Подключение OpenGL
Глава 2. Двумерные построения
Глава 3. Построения в пространстве
Глава 4. Визуальные эффекты
Глава 5. Пример CAD-системы: визуализация работы робота
Глава 6. Создаем свой редактор.
Заключение
Приложение 1. OpenGL в Интернете
Приложение 2. Список литературы


OpenGL в Delphi - Введение

Введение

Эта книга посвящена компьютерной графике, а именно тому, как использовать OpenGL в Delphi.
OpenGL - это стандартная библиотека для всех 32-разрядных операционных систем, в том числе и для операционной системы Windows.
OpenGL - не отдельная программа, а часть операционной системы. Это означает, что откомпилированное приложение, использующее OpenGL, не нуждается ни в каких дополнительных программах и модулях, кроме стандартных, содержащихся на любом компьютере с установленной операционной системой Windows 95 версии OSR2 и выше.
Вообще говоря, в этой книге идет речь о программировании приложений, использующих графический акселератор, однако все приводимые программы будут работать и на компьютере, не оснащенном ускорителем.
Для программистов, использующих язык С, существует множество источников, из которых можно почерпнуть сведения о том, как использовать библиотеку OpenGL, для программистов же, работающих с Delphi, таких источников крайне мало. Данная книга призвана восполнить этот недостаток информации.
В состав стандартной поставки Delphi (начиная с третьей версии) входит заголовочный файл, позволяющий строить приложения с использованием OpenGL, a также справочный файл по командам этой библиотеки. Однако инсталляция Delphi не снабжается ни одним примером по использованию OpenGL, a из файла справок новичку трудно понять, как это сделать. Поэтому основная цель книги - помочь программистам, в том числе и опытным, разобраться в этой теме.
В свое время, когда я сам учился использовать OpenGL в проектах Delphi, никаких источников, кроме набора текстов программ на языке С из файла оперативной помощи, у меня не было, и начинал я с того, что просто переносил эти программы на Delphi. Такая практика оказалась весьма полезной. Поэтому многие примеры в книге представляют собой "перевод" свободно распространяемых программ, изначально написанных на С. В текстах модулей этих примеров я оставил указание на авторов исходных версий.
В книге вы также встретите множество оригинальных программ. Кроме того, я по мере возможностей старался приводить и рекомендации профессионалов, содержащиеся в учебных программах пакета OpenGL SDK (Software Design Kit) и других учебных курсов, поскольку многие читатели не имеют возможности самостоятельно перенести эти программы на Delphi и, думаю, нуждаются в некоторой помощи.
Книга задумывалась как учебник, которого мне когда-то не хватало, и именно такой я ее и написал, в соответствии со своим опытом и пристрастиями.
Хотелось бы отметить следующие особенности книги.

1. Отсутствует описание математического аппарата компьютерной графики.
Мне не стоило бы большого труда в очередной раз переписать главы учебника по линейной алгебре, как это делается многими авторами книг по компьютерной графике. Подозреваю, что многие читатели просто пролистывают страницы книг, испещренные формулами и схемами. По моему убеждению, OpenGL позволяет нарисовать все, что угодно даже тем программистам, которые не помнят наизусть формулу транспонирования матрицы. Я не умаляю значения этих знаний, рано или поздно они вам понадобятся, но литературы по этой теме уже имеется предостаточно.
2. Эта книга не заменит документации по OpenGL или Delphi. Есть довольно много книг, представляющих собой просто перевод содержимого файлов оперативной помощи. Может быть, кому-то такие переводы и нужны, но только не программистам, для которых знание английского языка является необходимым условием профпригодности. Как правило, я ограничиваюсь краткими замечаниями, позволяющими понять суть излагаемой темы, а подробности всегда можно найти в справочных файлах.
3. Главный упор делается на практические примеры. Все проекты я предоставляю в виде исходных файлов. Примеров в книге более двухсот, и остановился я тогда, когда исчерпал объем дискеты. В среде программистов бытует мнение, что документацию следует читать только тогда, когда что-то не получается, и многие из них знакомство с новым средством разработки начинают сразу с попытки написания программы. Именно для таких специалистов книга подойдет как нельзя кстати. (При таком обилии примеров читателю, наверное, будет непросто восстановить в памяти, в каком из них содержится необходимый кусок кода. Поэтому главное назначение иллюстраций в книге - помочь в навигации по примерам. )
4. Это учебник, а не справочник. Материал книги я построил в расчете на то, что читатель будет знакомиться с ним последовательно. Хотя материал и разбит на тематические разделы, ко многим темам я обращаюсь многократно. Если первоначальное знакомство с какой-либо темой оказалось трудным, у читателя будет возможность разобраться с ней позднее.
5. Книга рассчитана на новичка в области машинной графики, но не новичка в программировании на Delphi. При изложении материала подразумевается, что читатель имеет навыки работы в Delphi, и чем увереннее он чувствует себя здесь, тем больше пользы сможет извлечь из этой книги. Heкоторые разделы, например функции API, могут показаться поначалу трудными. Однако в подавляющей части примеров особо сложные приемы программирования не используются, и они достаточно легко поддаются освоению.

В главе 1 книги описываются базовые механизмы операционной системы.
Знание этих механизмов необходимо для понимания того, как построить
минимальное приложение, использующее OpenGL.
Умудренные опытом программисты со стажем, пришедшие к Delphi после изрядной практики в Turbo Pascal, вряд ли найдут в этой главе что-то новое, за исключением разделов, непосредственно относящихся к OpenGL. Поэтому эту главу я рекомендую внимательно прочитать тем, кто пока не имеет достаточного опыта.
Сегодня студенты в большинстве учебных заведений начинают знакомство с программированием непосредственно с изучения Delphi, не повторяя весь путь, пройденный предыдущим поколением программистов.
Можно сказать, что система программирования Delphi явилась подлинной революцией, полностью изменившей взгляд на программирование и, в частности, на программирование для Windows. 3a прошедшие годы ряды программистов пополнились армией людей, способных быстро, подчас виртуозно, создать масштабное приложение, не имея особого понятия ни об архитектуре Windows, ни об основополагающих принципах работы приложения и его взаимодействия с операционной системой. В принципе, этих знаний и не требуется, чтобы создать приложение типа калькулятора или программы расчета напряжения в трубе. Однако при использовании OpenGL даже для построения минимальной программы необходимо иметь представление о базовых понятиях операционной системы.
Глава 2 посвящена примитивам OpenGL - базовым фигурам, из которых строятся объекты сцены. Собственно с этой главы и начинается рисование. Все примеры в ней плоскостные, однако пропускать ее не стоит, поскольку весь остальной материал предполагает наличие знаний и навыков, полученных при ее изучении. Материала главы достаточно для того, чтобы читатель смог построить график функции или чертеж автомобиля.
Глава 3 продолжает вводный курс по построениям в OpenGL - здесь читатель получит навыки трехмерной графики. Заканчивается глава разбором методов создания анимации. После изучения этой главы читатель сможет создавать уже довольно сложные модели, например, нарисовать автомобиль или самолет.
Глава 4 знакомит с тем, как приблизить качество изображения к фотореалистическому и как использовать OpenGL для создания специальных эффектов. Это самая важная глава книги. После усвоения ее материала читатель сможет нарисовать, например, модель Вселенной со всеми ее компонентами.
Глава 5 содержит пример построения сравнительно масштабного приложения, визуализирующего работу робототехнической установки Здесь читатель может получить представление о том, как создавать подобные приложения и как можно использовать OpenGL для "серьезных" целей Здесь же можно углубить знания по важнейшим понятиям операционной системы
Глава 6 освещает некоторые дополнительные темы использования OpenGL. такие как вывод текста и выбор Здесь же содержится еще один пример сравнительно большой программы - модельера, позволяющего из набора базовых объектов создавать сложные системы и автоматизировать подготовку кода для таких систем.


OpenGL в Delphi - ГЛАВА 1 Подключение OpenGL

ГЛАВА 1 Подключение OpenGL

Событие, сообщение, ссылка
Почему приложения Delphi имеют большой размер
Программирование на Delphi без VCL
Минимальная Windows-программа
Вывод с использованием функций GDI
Перехват сообщений
Работа с таймером
Работа с мышью и клавиатурой
DLL
Контекст устройства и контекст воспроизведения
Минимальная программа OpenGL
Формат пиксела
Решение проблем
Вывод на компоненты Delphi средствами OpenGL
Стили окна и вывод OpenGL
Полноэкранные приложения
Типы OpenGL
Тип TColor и цвет в OpenGL
Подробнее о заголовочном файле opengl. pas

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

Событие, сообщение, ссылка
С понятием "событие" знаком каждый программист, использующий Delphi. Термин "сообщение" напрямую в концепции Delphi не используется.
Очень часто это синонимы одного и того же термина операционной системы, общающейся с приложениями (окнами) посредством посылки сигналов, называемых сообщениями.
Код, написанный в проекте Delphi как обработчик события OnCreate, выполняется при получении приложением сообщения WM_CREATE, сообщению WM_PAINT соответствует событие OnPaint и т. д.
Такие события - аналоги сообщений операционной системы - используют мнемонику, сходную с мнемоникой сообщений, т. e. сообщения начинаются с префикса "WM_" (Windows Message), a аналогичные события начинаются с префикса "On".
Для того чтобы операционная система могла различать окна для осуществления диалога с ними, все окна при своем создании регистрируются в операционной системе и получают уникальный идентификатор, называемый "ссылка на окно". Тип этой величины в Delphi - HWND (Handle WiNDow). Синонимом термина "ссылка" является дескриптор.
Ссылка на окно может использоваться не только операционной системой, но и приложениями для идентификации окна, с которым необходимо производить манипуляции.
Попробуем проиллюстрировать смысл ссылки на окно на несложном примере.
Откомпилируйте минимальное приложение Delphi и начните новый проект. Форму назовите Form2, разместите на ней кнопку. Обработчик события нажатия кнопки OnClick приведите к следующему виду (готовый проект располагается на дискете в подкаталоге Ex01 каталога Chapter1):

procedure TForm2.ButtonlClick(Sender: TObject);
var
H : HWND; // ссылка на окно
begin
H := FindWindow ('TForm1', 'Form1'); // ищем окно
If H <> 0 then ShowMessage ('Есть Form1!') // окно найдено
else ShowMessage ('Нет Form1!') // окно не найдено
end;

Теперь при нажатии кнопки выдается сообщение, открыто ли окно класса, зарегистрированного в операционной системе как 'TForml1', имеющее заголовок 'Form1'. Если одновременно запустить обе наши программы, то при нажатии кнопки будет выдано одно сообщение, а если окно с заголовком 'Form1' закрыть, то другое.
Здесь мы используем функцию FindWindow, возвращающую величину типа HWND - ссылку на найденное окно либо ноль, если такое окно не найдено. Аргументы функции - класс окна и его заголовок. Если заголовок искомого окна безразличен, вторым аргументом нужно задать nil.
Итак, ссылка на окно однозначно определяет окно. Свойство Handle формы и есть эта ссылка, а тип THandle в точности соответствует типу HWND, так что в предыдущем примере переменную Н можно описать как переменную типа THandle.
Рассмотрим подробнее некоторые выводы. Класс окна минимального приложения, созданного в Delphi, имеет значение 'TForm1', что полностью соответствует названию класса формы в проекте. Следовательно, то, как мы называем формы в проектах Delphi, имеет значение не только в период проектирования приложения, но и во время его работы. Начните новый проект, назовите форму каким-нибудь очень длинным именем и откомпилируйте проект. Сравните размер откомпилированного модуля с размером самого
первого проекта, и убедитесь, что он увеличился - вырос только за счет длинного имени класса.
Также очень важно уяснить, что, если вы собираетесь распространять какие-либо приложения, необходимо взять за правило называть формы отлично от значения, задаваемого Delphi по умолчанию. Лучше, если эти названия будут связаны по смыслу с работой вашего приложения. Так, например, головную форму в примерах этой книги я буду называть, как правило, frmGL.
Имея ссылку на окно, операционная система общается с ним путем посылки сообщений - сигналов о том, что произошло какое-либо событие, имеющее отношение именно к данному окну. Если окно имеет намерение отреагировать на событие, операционная система совместно с окном осуществляет эту реакцию.
Окно может и, не имея фокус, получать сообщения и реагировать на них. Проиллюстрируем это на примере.
Обработчик события OnMouseMove формы приведите к следующему виду (проект находится в подкаталоге Ех02):

procedure TForm2.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer) ;
begin
Caption := 'x=' + IntToStr (X) + ', y=' + IntToStr (Y) // X, Y -
// координаты курсора
end;

При движении курсора мыши в заголовке формы выводятся его координаты.
Запустите два экземпляра программы и обратите внимание, что окно, не имеющее фокус, т. е. неактивное, тоже реагирует на перемещение указателя по своей поверхности и выводит в заголовке текущие координаты курсора в своей системе координат.
Имея ссылку на окно, приложение может производить с ним любые (почти) действия путем посылки ему сообщений.
Изменим код обработки щелчка кнопки (проект из подкаталога Ех03):

procedure TForm2.ButtonlClick(Sender: TObject);
var
H : HWND;
begin
H := FindWindow ('TForml', ' Forml');
If H <> 0 then SendMessage (H, WM_CLOSE, 0, 0)//закрыть найденное окно
end;

Если имеется окно класса 'TForm1' с заголовком 'Form1', наше приложение посылает ему сообщение WM_CLOSE - пытается закрыть окно. Для посылки сообщения используем функцию операционной системы (функцию API) SendMessage. Функция postMessage имеет сходное назначение, но отличается тем, что не дожидается, пока посланное сообщение будет отработано. У этих функций четыре аргумента - ссылка на окно, которому посылаем сообщение, константа, соответствующая посылаемому сообщению, и два параметра сообщения, смысл которых определяется в каждом конкретном сообщении по-своему. Параметры сообщения называются wParam и lParam. При обработке сообщения WM_CLOSE эти значения никак не используются, поэтому здесь их можно задавать произвольно.
Заметим, что одновременно могут быть зарегистрированы несколько окон класса 'TForm1', и необходимо закрыть их все. Пока наше приложение закрывает окна поодиночке при каждом нажатии на кнопку. Автоматизировать процесс можно разными способами, простейший из них используется в проекте подкаталога Ex04 и заключается в том, что вызов Findwindow заключен в цикл, работающий до тех пор, пока значение переменной H не станет равным нулю:

procedure TForm2. ButtonlClick(Sender: TObject);
var
H: HWND;
begin
Repeat
H: = FindWindow ('TForm1', 'Form1');
If H <> 0 then SendMessage (H, WM_CLOSE, 0, 0)
Until H = 0;
end;

Ну а как работать с приложениями, класс окна которых не известен, поскольку у нас нет (и не может быть) их исходного кода?
Для решения подобных проблем служит утилита Ws32, поставляемая с Delphi.
Например, с помощью этой утилиты я выяснил, что класс окна главного окна среды Delphi имеет значение 'TAppBuilder'. Узнав это, я смог написать проект, где делается попытка закрыть именно это окно (находится в подкаталоге Ex05).
Каждый раз я говорю именно о попытке закрыть окно, потому что приложение, получающее сообщение WM_CLOSE, может и не закрыться сразу же. Например, среда Delphi или текстовый процессор перед закрытием переспрашивают пользователя о необходимости сохранения данных. Но ведет себя приложение точно так же, как если бы команда поступала от пользователя.
В качестве следующего упражнения рассмотрите проект, располагающийся в подкаталоге Ex06, где по нажатию кнопки минимизируется окно, соответствующее минимальному проекту Delphi.
Для того чтобы минимизировать окно, ему посылается сообщение WM_SYSCOMMAND, соответствующее действию пользователя "выбор системного меню окна". Третий параметр функции SendMessage для минимизации окна необходимо установить в значение SC_MINIMIZE.
Работа с функциями API, сообщения Windows - темы весьма объемные. Пока мы рассмотрели только самые простейшие действия - закрыть и минимизировать окно.
В заключение раздела, необходимо сказать, что ссылки, в зависимости от версии Delphi, соответствуют типам Integer или LongWord и описываются в модуле windows. pas.

Почему приложения Delphi имеют большой размер
Этот вопрос часто задают начинающие программисты при сравнении приложений, созданных в различных средах программирования. Действительно, минимальное приложение, созданное в различных версиях Delphi, может достигать от 170 до 290 Кбайт. Это очень большая цифра для операционной среды Windows, в компиляторах C++ она составляет порядка 40 Кбайт. Конечно, это не катастрофическая проблема, когда емкости накопителей измеряются гигабайтами, и средний пользователь, как правило, не обращает внимания на размер файла. Неудобства возникают, например, при распространении приложений по сети.
Использование пакетов значительно снимает остроту проблемы для масштабных проектов, но суммарный вес приложения и используемых пакетов все равно значителен.
Краткий ответ на вопрос, поставленный в заголовке раздела, состоит в том, что большой размер откомпилированных приложений является платой за невероятное удобство проектирования, предоставляемое Delphi. Архитектура среды программирования, RTTI, компонентный подход - все это превращает Delphi в поразительно мощный инструмент. С помощью Delphi легко написать приложения, в которых, например, динамически создаются интерфейсные элементы любого типа (класса).
Однако приложения среднего уровня не используют и не нуждаются в этих мощных возможностях. Часто ли вам встречались приложения, предлагающие пользователю перед вводом/выводом данных определиться, с помощью каких интерфейсных элементов будет осуществляться ввод или вывод, а затем разместить эти элементы на окне в удобных местах? И пользователи, и разработчики в таких средствах, как правило, не испытывают необходимости.
Однако откомпилированный модуль содержит в себе весь тот код, благодаря которому в Delphi так легко производить манипуляции со свойствами и методами объектов. К примеру, если просмотреть содержимое откомпилированного модуля, то мы встретим в нем фразы, имеющие к собственно операционной системе косвенное отношение, например, "OnKeyDown" или другие термины Delphi.
Дело здесь не в несовершенстве компилятора, компилятор Delphi оптимизирует код превосходно, дело в самой идеологии Delphi.
Очень часто после выяснения этого факта начинающие программисты задают вопрос, как избавиться от RTTI, от включения "ненужного" кода в исполняемые модули.
К сожалению, это сделать невозможно. Кардинально проблема решается только через отказ от использования библиотеки классов Delphi, т. e. программирование без VCL.

Программирование на Delphi без VCL
После того как мы прикоснулись к основополагающим терминам и понятиям операционной системы Windows "сообщение" и "ссылка на окно", мы сможем опуститься ниже уровня объектно-ориентированного программирования, VCL и RAD-технологий. Требуется это по четырем причинам.
Во-первых, приложения, активно использующие графику, чаще всего не нуждаются и не используют богатство библиотеки классов Delphi. Таким приложениям, как правило, достаточно окна в качестве холста, таймера и обработчиков мыши и клавиатуры.
Во-вторых, при программировании, основанном только на использовании функций API, получаются миниатюрные приложения. Откомпилированный модуль не отягощается кодом описания компонентов и кодом, связанным с концепциями ООП.
В-третьих, для понимания приемов, используемых для увеличения скорости воспроизведения, нужно иметь представление о подлинном устройстве Windows-программы. Например, чтобы команды перерисовки окна выполнялись быстрее, мы будем избегать использования методов Refresh и paint формы.
В-четвертых, это необходимо для понимания действий, производимых для подключения OpenGL. Эта библиотека создавалась в эпоху становления ООП, и ее пока не коснулись последующие нововведения в технологии программирования.

Минимальная Windows-программа
Посмотрите на проект из подкаталога Ex07 - код минимальной программы Windows. Минимальной она является в том смысле, что в результате получается просто пустое окно. Также ее можно назвать минимальной программой потому, что откомпилированный модуль занимает всего около 16 Кбайт. Приложение меньшего размера, имеющее собственное окно, получить уже никак не удастся, хотя могут быть и программы еще короче и меньше, например, такая:

program p; uses Windows;
begin
MessageBeep(mb_ok)
end.

Единственное, что делает эта программа, - подача звукового сигнала. Однако вернемся к коду проекта из подкаталога Ex07. Первое, на что необходимо обратить внимание: в списке uses указаны только два модуля - windows и Messages. Это означает, что в программе используются исключительно функции API, и как следствие - длинный С-подобный код. И действительно, перенести эту и подобные ей программы на С потребует немного усилий.
Данная программа для нас крайне важна, поскольку она станет шаблоном для некоторых других примеров.
Программу условно можно разделить на две части - описание оконной функции и собственно головная программа.
В оконной функции задается реакция приложения на сообщения Windows. Именно оконную функцию необходимо дополнять кодом обработчиков сообщений для расширения функциональности приложения. Нечто подобное мы имеем в событийно-ориентированном программировании, но, конечно, в совершенно ином качестве.
В минимальной программе задана реакция на единственное сообщение wm_Destroy. На все остальные сообщения вызывается функция ядра операционной системы DefWindowProc, осуществляющая стандартную реакцию окна. Полученное окно ведет себя обычно, его можно изменять в размерах, минимизировать, максимизировать. Приложение реагирует также привычным образом, однако необходимости кодировать все эти действия нет.
В принципе, можно удалить и обработку сообщения wm_Destroy, но в этом случае приложение после завершения работы оставит след в памяти, съедающий ресурсы операционной системы.
Значение переменной-результата обнуляется в начале описания оконной функции для предотвращения замечания компилятора о возможной неинициализации переменной.
Головная программа начинается с того, что определяются атрибуты окна. Термин "структура", перешедший в Delphi из языка С, соответствует термину "запись". Термин "класс окна" имеет к терминологии объектно-ориентированного программирования скорее приближенное, чем непосредственное отношение.
Значения, задаваемые полям структуры, определяют свойства окна. В этой программе я задал значения всем полям, что, в принципе, делать не обязательно, мы обязаны указать адрес оконной функции, а все остальные значения можно брать по умолчанию. Однако в этом случае окно будет выглядеть или вести себя необычно. Например, при запуске любого приложения операционная система задает курсор для него в виде песочных часов, и если мы не станем явно задавать вид курсора в классе окна, курсор окна приложения так и останется в виде песочных часов.
После заполнения полей класса окна его необходимо зарегистрировать в операционной системе.
В примере я анализирую результат, возвращаемый функцией Registerclass. Это также делать не обязательно, невозможность регистрации класса окна - ситуация крайне редкая при условии корректного заполнения его полей.
Следующие строки можно интерпретировать как "создание конкретного экземпляра на базе зарегистрированного класса" Очень похоже на ООП, но схожесть эта весьма приблизительная и объясняется тем, что первая версия Windows создавалась в эпоху первоначального становления концепции объектно-ориентированного программирования.
При создании окна мы уточняем его некоторые дополнительные свойства - заголовок, положение, размеры и прочее. Значения этих свойств задаются аргументами функции createWindow, возвращающей внимание, величину типа HWND - ту самую ссылку на окно, что в Delphi называется Handle. После создания окна его можно отобразить - вызываем функцию showWindow. Как правило, окно сразу после этого перерисовывают вызовом функции updateWindow - действие тоже необязательное, но для корректной работы приложения удалять эту строку нежелательно.
Далее следует цикл обработки сообщений, наиважнейшее место в программе, фактически это и есть собственно работа приложения. В нем происходит диалог приложения с операционной системой: извлечение очередного сообщения из очереди и передача его для обработки в оконную функцию. Как уже говорилось, функции API и сообщения - темы очень обширные, и я не ставлю целью исчерпывающе осветить эти темы. В разумных объемах я смогу изложить только самое необходимое, а более подробную информацию можно получить в оперативной помощи Delphi.
К сожалению, версии Delphi 3 и 4 поставляются с системой помощи, не настроенной должным образом для получения информации по функциям API и командам OpenGL. Если судить по содержанию помощи, то может сложиться впечатление, что эти разделы в ней вообще отсутствуют.
Можно либо настраивать справочную систему самостоятельно, либо, что я и предлагаю, пользоваться контекстной подсказкой - для получения сведений по любой функции API достаточно поставить курсор на соответствующую строку и нажать клавишу <F1>.

Замечание
В пятой версии Delphi система помощи настроена вполне удовлетворительно, а сами файлы помощи обновлены

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

Замечание
Итак, в приложениях Windows на самом деле управление постоянно находится в цикле обработки сообщений, событийно-ориентированная схема как таковая отсутствует. При получении окном очередного сообщения управление передается оконной функции, в которой задается реакция на него, либо вызывается функция API DefWindowProc для реакции, принятой по умолчанию

Приведенный пример лишь отдаленно напоминает то, что мы имеем в Delphi - событийно-ориентированное программирование, основанное на объектах. Конечно, совершить путь, обратный исторически пройденному, нелегко. Отказаться от библиотеки VCL при написании программ на Delphi для многих оказывается непосильным. Вознаграждением здесь может стать миниатюрность полученных программ: как мы видим, минимальная программа уменьшилась минимум в десять раз, быстрее загружается, выполняется и быстрее выгружается из памяти.
Такой размер нелегко, а порой и невозможно получить в любом другом компиляторе, кроме как в Delphi. К тому же проекты, в списке uses которых стоят только windows и Messages, компилируются еще стремительнее, несмотря на устрашающую массивность кода.
А сейчас посмотрите проект из подкаталога Ex08, где окно дополнено кнопкой и меткой. В коде появились новые строки, а простейшие манипуляции, например, изменение шрифта метки, еще потребуют дополнительных строк.
Подобный обширный код обычно обескураживает новичков, поэтому я не буду злоупотреблять такими примерами и ограничусь только самыми необходимыми для нас темами - как создать обработчик мыши, клавиатуры и таймера.
Я не буду заставлять вас писать все программы таким изнурительным способом, нам просто нужно иметь представление о работе базовых механизмов, чтобы лучше понимать, что делает за нас Delphi и что необходимо сделать, чтобы подключить OpenGL к нашим проектам.

Вывод с использованием функций GDI
В первом разделе мы рассмотрели, как, получив ссылку на чужое окно, можно производить с ним некоторые действия, например, закрыть его.
Точно так же, если необходимо нарисовать что-либо на поверхности чужого окна, первым делом нужно получить ссылку на него.
Для начала попробуем рисовать на поверхности родного окна.
Разместите еще одну кнопку, обработку щелчка которой приведите к следующему виду (проект из подкаталога Ex09):

procedure TForma2. Button2Click(Sender: TObject);
var
dc: HDC; // ссылка на контекст устройства
begin
dc: = GetDC (Handle); // задаем значение ссылки
Rectangle (dc, 10, 10, 110, 110); // рисуем прямоугольник
ReleaseDC (Handle, dc);
// освобождение ссылки DeleteDC (dc);
// удаление ссылки, освобождение памяти
end;

Запустите приложение. После щелчка на добавленной кнопке на поверхности окна рисуется квадрат.
Для рисования в этом примере используем низкоуровневые функции вывода Windows, так называемые функции GDI (Graphic Device Interface, интерфейс графического устройства). Эти функции требуют в качестве одного из своих аргументов ссылку на контекст устройства.
Тип такой величины - hdc (Handle Device Context, ссылка на контекст устройства), значение ее можно получить вызовом функции API GetDC с аргументом-ссылкой на устройство вывода. В нашем примере в качестве аргумента указана ссылка на окно.
После получения ссылки на контекст устройства обращаемся собственно к функции, строящей прямоугольник. Первым аргументом этой функции является ссылка на контекст устройства.
После использования ссылки ее необходимо освободить, а в конце работы приложения - удалить для освобождения памяти.
Поставленная "клякса" будет оставаться на окне формы при изменении размеров окна и исчезнет только при его перерисовке, для чего можно, например, минимизировать окно формы, а затем вновь его развернуть. Исчезновение квадрата после такой операции объясняется тем, что за перерисовку окна отвечает его собственный обработчик.
Теперь попробуем порисовать на поверхности чужого окна, для чего изменим только что написанный код (готовый проект находится в подкаталоге Ех10):

procedure TForm2. Button2Click(Sender: TObject);
var
dc: HDC; Window: HWND; begin
Window: = FindWindow ('TForml', 'Forml'); If Window <> 0 then begin // окно найдено
dc: = GetDC (Window); // ссылка на найденное окно
Rectangle (dc, 10, 10, 110, 110); // квадрат на чужом окне
ReleaseDC (Window, dc); // освобождение ссылки
DeleteDC (dc); // удаление ссылки
end;
end;

Теперь при щелчке на кнопке, если в системе зарегистрировано хотя бы одно окно класса 'TForm1' с заголовком 'Form1', вывод (рисование квадрата) будет осуществляться на него.
Запустите параллельно откомпилированные модули минимального и только что созданного приложений. При щелчке на кнопке квадрат рисуется на поверхности чужого окна.
Замечу, что если закрыть Projectl. exe и загрузить в Delphi соответствующий ему проект, то при щелчке на кнопке прямоугольник будет рисоваться на поверхности окна формы, что будет выглядеть необычно.
Этот эксперимент показывает, что окна, создаваемые Delphi во время проектирования, такие же равноправные окна, как и любые другие, т. e. они регистрируются в операционной системе, идентифицируются, и любое приложение может иметь к ним доступ. Если попытаться минимизировать окно класса 'TForm1', окно формы будет отрабатывать эту команду точно так же, как полученную от пользователя.
Следует обратить внимание на то, что мы не можем рисовать на поверхности вообще любого окна. Например, не получится, поменяв имя класса окна на 'TAppBuilder', поставить "кляксу" на главном окне среды Delphi.
Окно со значением ссылки, равным нулю, соответствует окну рабочего стола. В примере Exll я воспользовался этим, чтобы нарисовать квадратик на рабочем столе:

procedure TForm2. Button2Click(Sender: TObject);
var
dc: HDC;
begin
dc: = GetDC (0); // получаю ссылку на рабочий стол
Rectangle (dc, 10, 10, 110, 110);
ReleaseDC (Handle, dc);
DeleteDC (DC);
end;

Во всех примерах этого раздела я для вывода использовал функции GDI потому, что если для вывода на родном окне Delphi и предоставляет удобное средство - методы свойства формы canvas, то для вывода на чужом окне мы этими методами воспользоваться не сможем в принципе. Разберем основные детали вывода с помощью функций GDI. B проекте из подкаталога Exl2 оконная функция минимальной программы дополнилась обработкой сообщения WM_PAINT. Вывод заключен между строками с вызовом функций BeginPaint и EndPaint, первая из которых возвращает ссылку на контекст устройства, т. e. величину типа HDC, требуемую для функций вывода GDI. Еще раз повторю, что после использования ссылки ее необходимо освободить и удалить по окончании работы - это необходимо для корректной работы приложения. Смысл ссылки на контекст устройства мы подробно обсудим немного позже.
Остальные строки кода подробно прокомментированы, так что, надеюсь, особых вопросов не вызовут, разве только начинающих может в очередной раз поразить обширность кода, возникающая при отказе от удобств, предоставляемых библиотекой классов Delphi (которая выполняет за программиста всю черновую работу и превращает разработку программы в сплошное удовольствие).

Перехват сообщений
Большинство событий формы и компонентов являются аналогами соответствующих сообщений операционной системы.
Конечно, не все сообщения имеют такие аналоги, поскольку их (сообщений) очень много, несколько сотен. С каждой новой версией Delphi y формы появляются все новые и новые свойства и события, благодаря чему программировать становится все удобнее, но зато размеры откомпилированного приложения все растут и растут.

Замечание
В угоду программистам, до сих пор использующим третью версию Delphi для получения сравнительно небольших по объему исполняемых модулей, все примеры данной книги я разрабатывал именно в этой версии, но все они прекрасно компилируются и в более старших версиях

У программистов всегда будет возникать потребность обрабатывать сообщения, не имеющие аналогов в списке событий, либо самостоятельно перехватывать сообщения, для которых есть аналоги среди событий формы и компонентов. Как увидим ниже, сделать это несложно.
Для начала обратимся к проекту из подкаталога Exl3, где мы опять будем программировать без VCL. Задача состоит в том, чтобы при двойном щелчке левой кнопкой мыши выводились текущие координаты указателя. Прежде всего обратите внимание, что в стиль окна добавилась константа cs_Dblclks, чтобы окно могло реагировать на двойной щелчок, а оконная функция дополнилась обработкой сообщения wm_LButtonDblclk, в которой выводятся координаты курсора.
Теперь создадим обработчик этого же сообщения в проекте Delphi обычного типа (проект располагается в подкаталоге Exl4).
Описание класса формы я дополнил разделом protected, в который поместил forward-описание соответствующей процедуры:

procedure MesDblClick (var MyMessage TWMMouse); message
wm LButtonDblClk;

Замечание
Как правило, перехватчики сообщений для повышения надежности работы приложения описываются в блоке protected

Имя процедуры я задал таким, чтобы не появлялось предупреждение компилятора о том, что я перекрываю соответствующее событие формы.
Служебное слово message указывает на то, что процедура будет перехватывать сообщение, мнемонику которого указывают за этим словом. Тип аргумента процедуры-перехватчика индивидуален для каждого сообщения. Имя аргумента произвольно, но, конечно, нельзя брать в качестве имени служебное СЛОВО message.
Пожалуй, самым сложным в процессе описания перехвата сообщений является определение типа аргумента процедуры, здесь оперативная помощь оказывается малополезной. В четвертой и пятой версиях Delphi инспектор кода облегчает задачу, но незначительно.
Чтобы решить эту задачу для сообщения wm_LButtonDblclk, я просмотрел все вхождения фразы "LButtonDblClk" в файле messages. pas и обнаружил строку, подсказавшую решение:

TWMLButtonDblClk = TWMMouse;

В этом же файле я нашел описание структуры Twmouse, чем и воспользовался при кодировании процедуры MesDblclick для получения координат курсора Обратите внимание, что здесь не пришлось самостоятельно разбивать по словам значение параметра, как в предыдущем проекте Итак, в рассматриваемом примере перехватывается сообщение "двойной щелчок левой кнопки мыши". Событие Dblclick формы наступает точно в такой же ситуации. Выясним, какая из двух процедур, перехватчик сообщения или обработчик события, имеет преимущество или же они равноправны. Создайте обработчик события OnDblclick формы - вывод любого тестового сообщения (можете воспользоваться готовым проектом из подкаталога Exl5). Запустите проект, дважды щелкните на форме. Процедура-перехватчик среагирует первой и единственной, до обработчика события очередь не дойдет.

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

В проекте из подкаталога Exl6 создан обработчик сообщения wmPaint - перерисовка окна:

protected
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
...
procedure TForml. WMPaint(var Msg: TWMPaint);
var
ps: TPaintStruct;
begin
BeginPaint(Handle, ps);
Rectangle (Canvas. Handle, 10, 10, 100, 100);
EndPaint(Handle, ps);
end;

Строки Beginpaint и EndPaint присутствуют для более корректной работы приложения, при их удалении появляется неприятное мерцание при изменении размеров окна. Обратите внимание на функцию построения прямоугольника: я воспользовался тем, что свойство canvas. Handle и есть ссылка на контекст устройства, соответствующая окну формы
Точно так же, как перехватчики сообщений предпочтительнее обработчиков событий, использование непосредственно ссылок на окно и ссылок на контекст устройства предпочтительнее использования их аналогов из мира ООП.
Запомните этот пример, таким приемом мы будем пользоваться очень часто

Работа с таймером
В этом разделе мы разберем, как использовать таймер, основываясь только на функциях API Поскольку вы наверняка умеете работать с компонентом класса TTimer, вам легко будет уяснить, как это делается на уровне функций API.
Посмотрите простой пример, располагающийся в подкаталоге Exl7, где с течением времени меняется цвет нарисованного кружочка. Первым делом замечаем, что блок описания констант дополнился описанием идентификатора таймера, в качестве которого можно взять любое целое число.

const
AppName = 'WinPaint';
id_Timer = 100; // идентификатор таймера

Идентифицировать таймер необходимо потому, что у приложения их может быть несколько. Для включения таймера (то, что в привычном антураже соответствует Timerl. Enabled: = True) вызывается функция API SetTimer, где задается требуемый интервал таймера:

SetTimer (Window, id_Timer, 200, nil); // установка таймера

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

wm_Timer: InvalidateRect (Window, nil, False);

Если в приложении используется несколько таймеров, необходимо отделять их по значению идентификатора, передаваемому в wParam.
В моем примере каждые 200 миллисекунд окно перерисовывается вызовом функции API invalidateRect Запомните этот прием, потом мы не раз будем его использовать. Изменение цвета кружочка достигается тем, что при каждой перерисовке объект "кисть" принимает новое значение

Brush: = CreateSolidBrush (RGB(random (255), random (255), random(255)));

Как всегда в Windows, созданные объекты должны по окончании работы удаляться, дабы не поглощали ресурсы Для удаления таймера вызываем Функцию KillTimer в обработчике сообщения wm_Destroy:

KillTimer (Window, id_Timer);

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

Работа с мышью и клавиатурой
Как обработать двойной щелчок левой кнопки мыши, опираясь на сообщения, мы рассмотрели выше в разделе "Перехват сообщений" данной главы. Проект из подкаталога Exl8 является примером на обработку остальных сообщений, связанных с мышью. При нажатой левой кнопки мыши за указателем остается след. Оконная функция дополнилась обработчиками сообщений wm_LButtonDown, wm_LButtonUp И wm_MouseMove Для определения координат курсора пользуемся тем, что поле iParam подобных сообщений содержит эти самые координаты.

Down: = not Down;
wm_Create: Down: = False;
wm_LButtonDown, wm_LButtonUp
wm_MouseMove: begin
If Down then begin xpos: = LoWord ( LParam);
ypos: = HiWord ( LParam);
InvalidateRect(Window, nil, False);
end;
end;
wm_Paint: begin
If Down then begin
dc: = BeginPaint (Window, MyPaint);
Ellipse (dc, xPos, yPos, xPos + 2, yPos + 2);
EndPaint (Window, MyPaint);
ReleaseDC (Window, dc);
end;
end;

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

wm_Char: // анализ нажатой клавиши
case wParam of
$58, $78: If HiWord {GetKeyState (vk_Shift)) = 0 { Shift }
then MessageBox(Window, 'X', 'Нажата клавиша', MB_OK)
else MessageBox(Window, 'X вместе с Shift', 'Нажата клавиша', MB_OK);
end; // wm char

При нажатии клавиши 'X' выводится сообщение, в котором указано, нажата ли одновременно клавиша <Shift>. Я использовал шестнадцатеричное представление кода клавиши, но, конечно, можно использовать и десятичное. Надеюсь, здесь не требуются особые пояснения, и мы сможем использовать этот код в качестве шаблона в будущих проектах.

DLL
Файлы DLL (Dynamic Link Library, библиотека динамической компоновки) являются основой программной архитектуры Windows и отличаются от исполняемых файлов фактически только заголовком

Замечание
Но это не означает, что если переименовать DLL-файл, то он станет исполняемым имеется в виду заголовочная информация файла

Для загрузки операционной системы необходимо запустить файл win com, имеющий размер всего 25 Кбайт Как легко догадаться, в файл такого размера невозможно поместить код, реализующий всю ту гигантскую работу, которая производится по ходу выполнения любого приложения Этот файл является загрузчиком ядра операционной системы, физически размещенным в нескольких DLL-файлах.
Помимо кода, DLL-файлы могут хранить данные и ресурсы. Например, при изменении значка (ярлыка) пользователю предлагается на выбор набор значков из файла SHELL32. DLL.
Как мы уже знаем, для создания любой программы Windows, имеющей собственное окно, в проекте необходимо подключать как минимум два модуля' windows и Messages Первый из этих файлов содержит прототипы функций API и GDI Посмотрим на прототип одной из них:

function CreateDC;
external gdi32 name 'CreateDCA';
Здесь величина gdi32 - константа, описанная в этом же модуле'
const
gdi32 = 'gdi32. dll';

Таким образом, функция CreateDC физически размешена в файле gdi32 dll и каждое приложение, использующее функции GDI, обращается к этой библиотеке.
Приблизительно так же выглядят прототипы остальных функций и процедур, но указываемые в прототипе имена библиотек индивидуальны для каждой из них.
Обратите внимание, что в этом же файле находится описание константы opengl32 Использование DLL, в частности, позволяет операционной системе экономить память.
Приложение не умеет ни создавать окно, ни выводить в него информацию и не содержит кода для этих действий. Все запущенные приложения (клиенты) передают соответствующие команды файлу gdi32. dll (серверу), который отрабатывает их, осуществляя вывод на устройство, ссылку на контекст которого передается в качестве первого аргумента функции. При этом клиентов обслуживает единственная копия сервера в памяти. Помимо важности DLL как основы программной архитектуры операционной системы, представление о динамических библиотеках необходимо иметь каждому разработчику программных систем. Многие программные системы строятся по следующему принципу: основной код и данные размещаются в динамических библиотеках, а исполняемому файлу отводится роль загрузчика. Подробнее о такой организации мы поговорим в главе 5. Библиотека OpenGL физически также размещена в виде двух DLL-файлов: opengl23. dll и glu32. dll. Первый из этих файлов и есть собственно библиотека OpenGL. Назначение его - осуществление взаимодействия с акселератором или программная эмуляция ускорителя за счет центрального процессора. Поддержка ЗD-акселерации осуществляется с помощью полного (устанавливаемого) клиентского драйвера (Installable Client Driver, ICD) и мини-драйвера (Mini-Client Driver, MCD).
Библиотека OpenGL реализована по клиент-серверной схеме, т. e. ее одновременно может использовать несколько приложений при единственной копии сервера в памяти или вообще при удаленном расположении сервера (сервер в принципе может располагаться и не на компьютере клиента). Иногда программисту, как, например, в случае с OpenGL, бывает необходимо просмотреть список функций и процедур, размещенных в конкретном файле DLL. Для этого можно воспользоваться либо утилитой быстрого просмотра, поставляемой в составе операционной системы, либо утилитой tdump. exe, поставляемой в составе Delphi.
Для вызова утилиты быстрого просмотра установите курсор мыши на значок нужного файла и нажмите правую кнопку. В появившемся меню должен присутствовать пункт "Быстрый просмотр", если этого пункта нет, то требуется установить компонент операционной системы "Быстрый просмотр". Для использования утилиты tdump скопируйте ее и необходимый dll-файл в отдельный каталог. Запустите его с параметрами <имя анализируемого файла> и <имя файла-результата>, например:

TDUMP. EXE opengl32. dll opengl. txt

В файле opengl. txt будет выдана информация, извлеченная утилитой из заголовка библиотеки, аналогичная той, что выводится утилитой быстрого просмотра. Информация группируется по секциям, среди которых наиболее
часто программисту требуется секция экспортируемых функций для уточнения содержимого библиотеки.
Итак, чаще всего DLL представляет собой набор функций и процедур. Как говорится в справке Delphi по DLL, "динамические библиотеки являются идеалом для многоязыковых проектов". Это действительно так: при использовании OpenGL совершенно безразлично, в какой среде созданы сама библиотека и вызывающие ее модули.

Контекст устройства и контекст воспроизведения
Мы уже знаем, что ссылка на контекст устройства - это величина типа HDC. Для ее получения можно вызвать функцию GetDC, аргументом которой является ссылка на нужное окно.
Ссылке на контекст устройства соответствует свойство canvas. Handie формы, принтера и некоторых компонентов Delphi.
Каков же все-таки смысл контекста устройства, если он и так связан с однозначно определенным объектом - окном, областью памяти или принтером, и зачем передавать дополнительно какую-то информацию об однозначно определенном объекте?
Для ответа на эти вопросы обратим внимание на замечательное свойство вывода в Windows, состоящее в том, что одними и теми же функциями осуществляется вывод на различные устройства. Строки программы

Forml. Canvas. Ellipse (0, 0, 100, 100);

И

Printer. BeginDoc;
Printer. Canvas. Ellipse (0, 0, 100, 100);
Printer. EndDoc;

рисуют один и тот же круг как на поверхности формы, так и в распечатываемом документе, т. e. на различных устройствах, причем если мы будем выводить разноцветную картинку на монохромный принтер, он справится с этой задачей, передавая цвета оттенками серого.
Даже если мы рисуем только на поле формы, мы имеем дело с различными устройствами - нам неизвестно, какова графическая плата компьютера и каковы характеристики текущей установки настроек экрана. Например, имея в своем распоряжении более 16 миллионов цветов, приложение не заботится об отображении этой богатой палитры на экране, располагающем всего 256 цветами. Такие вопросы приложение перекладывает на плечи операционной системы, решающей их посредством использования драйверов устройств
Для того чтобы воспользоваться функциями воспроизведения Windows, приложению необходимо только указать ссылку на контекст устройства, содержащий средства и характеристики устройства вывода.
Справочный файл Win32 Programmer's Reference фирмы Microsoft, поставляемый в составе Delphi, o контексте устройства сообщает следующее "Контекст устройства является структурой, которая определяет комплект графических объектов и связанных с ними атрибутов и графические режимы, влияющие на вывод Графический объект включает в себя карандаш для изображения линии, кисть для закраски и заполнения, растр для копирования или прокрутки частей экрана, палитру для определения комплекта доступных цветов, области для отсечения и других операций, маршрут для операций рисования"
В OpenGL имеется аналогичное ссылке на контекст устройства понятие ссылка на контекст воспроизведения
Графическая система OpenGL, как и любое другое приложение Windows (хоть и размещенное в DLL), также нуждается в ссылке на устройство, на которое будет осуществляться вывод Это специальная ссылка на контекст воспроизведения - величина типа HGLRC (Handle openGL Rendering Context, ссылка на контекст воспроизведения OpenGL).

Замечание
Контекст устройства Windows содержит информацию, относящуюся к графическим компонентам GDI, a контекст воспроизведения содержит информацию, относящуюся к OpenGL, т e играет такую же роль, что и контекст устройства для GDI
В частности, упомянутые контексты являются хранилищами состояния системы, например, хранят информацию о текущем цвете карандаша

Минимальная программа OpenGL
Рассмотрев основные вопросы функционирования приложения и его взаимодействия с операционной системой, мы можем перейти к изучению собственно OpenGL. Заглянем в подкаталог Ex20, содержащий проект минимальной программы, использующей OpenGL B программе с помощью команд OpenGL окно формы окрашивается в голубоватый цвет Во-первых, обратите внимание на то, что список uses дополнен модулем OpenGL - это программист должен сделать сам Раздел private описания класса формы содержит строку

hrc: HGLRC; // ссылка на контекст воспроизведения

Смысл этой величины мы рассмотрели в предыдущем разделе. Обработчик события OnCreate формы содержит следующие строки:

SetDCPixelFormat(Canvas. Handle); //задаем формат пиксела
hrc: = wglCreateContext(Canvas. Handle); // создаем контекст воспроизведения

Первая строка - обращение к описанной в этом же модуле пользовательской процедуре, задающей формат пиксела

procedure SetDCPixelFormat (hdc: HDC);
var
pfd: TPixelFormatDescriptor;
nPixelFormat: Integer;
begin
FillChar (pfd, SizeOf (pfd), 0);
nPixelFormat: = ChoosePixelFormat (hdc, @pfd);
SetPixelFormat (hdc, nPixelFormat, @pfd);
end;

По поводу формата пиксела мы подробнее поговорим в следующем разделе Во второй строке обработчика OnCreate, как ясно из комментария, задается величина типа HGLRC, т. e. создается контекст воспроизведения. Аргументом функции wglCreateContext является ссылка на контекст устройства, на который будет осуществляться вывод Сейчас устройством вывода служит окно формы Для получения этого контекста OpenGL необходима величина типа HDC Здесь, как и во многих других примерах, мы используем факт, что canvas. Handle и есть ссылка на контекст устройства, связанная с окном формы.
Поскольку это первая функция, имеющая непосредственно отношение к OpenGL, то я немного отвлекусь на некоторые общие пояснения Как уже говорилось, только начиная с пятой версии Delphi поставляется с системой помощи, удовлетворительно настроенной с точки зрения получения справок по командам OpenGL и функциям API, в предыдущих версиях ее вроде как и нет Однако на самом деле такая помощь доступна и в ранних версиях, и самый простой способ ее получения - контекстная подсказка По командам OpenGL справки мы будем получать точно так же, как и по функциям API, т. e если вам необходима более подробная информация, например, о функции wglcreateContext, то установите курсор на строку с этой функцией и нажмите клавишу <F1>.
Функция wgicreateContext физически размещается в файле opengl32 dll, а прототип ее находится в файле windows. pas В этот файл также помещены прототипы всех функций и процедур, имеющих отношение к реализации OpenGL под Windows, a прототипы собственно команд OpenGL расположены в файле opengl pas функции и процедуры, имеющие отношение только к Windows-версии OpenGL, обычно имеют приставку wgl, как, например, wglcreateContext, но могут и не иметь такой приставки, как, например, SwapBuffers. Собственно команды OpenGL имеют приставки gl или glu в зависимости от размещения в библиотеках opengl32. dll и glu32. dll, соответственно. Итак, контекст воспроизведения создан, и теперь можно осуществлять вывод командами OpenGL. Обработка события onpaint выглядит следующим образом:

wglMakeCurrent (Canvas. Handle, hrc); // установить контекст
glClearColor (0. 5, 0. 5, 0. 75, 1. 0); // цвет фона
glClear (GL_COLOR_BUFFER_BIT); // очистка буфера цвета
wglMakeCurrent (0, 0); // освободить контекст

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

Замечание
Согласно справке, для освобождения контекста воспроизведения оба параметра должны быть установлены в NULL, но хотя компилятор и пропустит такие значения, во время выполнения получим ошибку "Invalid variant type conversion", так что будем всегда для освобождения контекста задавать эти значения нулевыми Обработка события onDestroy формы состоит из одной строки:

wglDeleteContext (hrc);

Тем самым мы по завершении работы приложения удаляем контекст воспроизведения, освобождая память.

Замечание
Очень важно запомнить, что процедуры и функции, имена которых начинаются на gl или glu, т e команды OpenGL, имеют какой-либо результат только при установленном контексте воспроизведения

Вернемся к команде glclearcolor, определяющей цвет фона. У нее четыре аргумента, вещественные числа, первые три из которых задают долю красного, зеленого и синего в результирующем цвете. О четвертом аргументе мы поговорим в четвертой главе, здесь я его значение задал равным единице. Можете варьировать это значение произвольно, в данном примере это никак не скажется, так что пока можете просто не обращать внимания на этот аргумент. Согласно справке, все четыре аргумента функции glclearColor имеют тип GLclampf, соответствующий вещественным числам в пределах от нуля до единицы. О типах OpenGL подробнее поговорим ниже
Теперь обратитесь к проекту из подкаталога Ex21, представляющему собой еще один вариант минимальной программы Delphi, использующей OpenGL, но без VCL. Надеюсь, читатель, вы уже можете ориентироваться в таких программах. Отличие от предыдущего примера состоит в том, что приходится самому описывать все те действия, которые при обычном подходе выполняет за нас Delphi, т. e. в данном случае "вручную" создавать ссылку на контекст устройства, устанавливать, освобождать и удалять ее.
Если вы используете Delphi версии три или четыре, вы, возможно, столкнетесь с одной небольшой проблемой. Если запускать проекты, использующие OpenGL, под управлением среды Delphi, программа может случайным образом аварийно завершаться. Оборот "случайным образом" здесь я употребил постольку, поскольку один и тот же проект может привести к аварийному завершению, а может и работать вполне успешно.
Я сталкивался с этой проблемой на компьютерах с различной конфигурацией и с различными версиями операционной системы, и, по-видимому, она связана с некорректным взаимодействием среды Delphi c драйверами. Если подобная проблема возникла и у вас, я рекомендую просто не запускать под управлением среды проекты, использующие OpenGL, a запускать собственно откомпилированные модули. В пятой версии Delphi такая ситуация не возникала, так что, по-видимому, этот недостаток разработчиками выявлен и устранен.

Формат пиксела
Напомню, ссылка на контекст устройства содержит характеристики устройства и средства отображения. Упрощенно говоря, получив ссылку на контекст устройства, мы берем в руки простой либо цветной карандаш или кисть с палитрой в миллионы оттенков.
Сервер OpenGL, прежде чем приступать к работе, также должен определиться, на каком оборудовании ему придется работать Это может быть скромная персоналка, а может быть и мощная графическая станция
Прежде чем получить контекст воспроизведения, сервер OpenGL должен получить детальные характеристики используемого оборудования. Эти характеристики хранятся в специальной структуре, тип которой - TPlxelFormatDescriptor (описание формата пиксела). Формат пиксела определяет конфигурацию буфера цвета и вспомогательных буферов.
Наберите в тексте модуля фразу "PixelFormatDescriptor", нажмите клавишу <Fl>, и вы получите подробную информацию об этом типе Обращаю внимание: мы видим раздел справки Microsoft, рассчитанной на программистов, использующих С или C++, поэтому описание содержит термины и стилистику именно этих языков. По традиции Delphi имена типов начинаются с префикса Т, но нам неудасться найти помощь по термину TPixelFormatDescnptor.
К сожалению, это не единственное неудобство, которое придется испытать Например, если мы заглянем в файл windows. pas и найдем описание записи TPixelFormatDescriptor, то обнаружим, что в файле помощи не указаны некоторые константы, имеющие отношение к этому типу, а именно:

PFD_SWAP_LAYER_BUFFERS,
PFD_GENERIC_ACCELERATED и PFD_DEPTH_DONTCARE.

А константа, названная PFD_DOUBLE__BUFFER_DONTCARE, по-видимому, соответствует константе, описанной в модуле windows. pas как PFD_DOUBLEBUFFER
DONTCARE.
Итак, смысл структуры pixelFormatDescriptor - детальное описание графической системы, на которой происходит работа. Вас может озадачить дотошность этого описания, но, уверяю, особое внимание из всего этого описания требуют совсем немногие вещи.
В проекте я привел описание всех полей структуры TPixelFormatDescriptor на русском языке (в момент их первоначального заполнения). Делается это в процедуре setDCPixelFormat, вызываемой между получением ссылки на контекст устройства и созданием ссылки на контекст воспроизведения OpenGL.
Посмотрим подробнее, что там делается. Полям структуры присваиваются желаемые значения, затем вызовом функции choosePixelFormat осуществляется запрос системе, поддерживается ли на данном рабочем месте выбранный формат пиксела, и, наконец, вызовом функции SetPixelFormat устанавливается формат пиксела в контексте устройства.
Функция choosePixelFormat возвращает индекс формата пиксела, который нам нужен в качестве аргумента функции SetPixelFormat.
Заполнив поля структуры TPixelFormatDescriptor, мы определяемся со своими пожеланиями к графической системе, на которой будет происходить работа приложения, OpenGL подбирает наиболее подходящий к нашим пожеланиям формат и устанавливает уже его в качестве формата пиксела для последующей работы. Наши пожелания корректируются сервером OpenGL применительно к реальным характеристикам системы.
To, что OpenGL не позволит нам установить нереальный для конкретного рабочего места формат пиксела, значительно облегчает нашу задачу. Предполагая, что разработанное приложение будет работать на машинах разного класса, можно запросить "всего побольше", а уж OpenGL разберется в каждом конкретном случае, каковы параметры и возможности оборудования, на котором в данный момент выполняется приложение.
На этом можно было бы и закончить разговор о формате пиксела, если бы мы могли полностью довериться выбору OpenGL.
Обратим внимание на поле структуры "битовые флаги", dwFlags. To, как мы зададим значение флагов, может существенно сказаться на работе нашего
приложения, и наобум задавать эти значения не стоит. Тем более что некоторые флаги совместно "не уживаются", а некоторые присутствуют только в паре с определенными флагами.
В рассматриваемом примере я присвоил флагам значение PFD_DRAW_TO WINDOW or PFD_SUPPORT_OPENGL, сообщив тем самым системе, что собираюсь осуществлять вывод в окно и что моя система в принципе поддерживает OpenGL (рис. 1. 1).

Рис. 1. 1. Поумолчанию режим двойной буферизации не установлен
Я ограничился всего двумя константами из обширного списка, приведенного в модуле windows. pas. (В файле справки почти для каждой из них имеется детальное описание. )
Так, константа PFD_DOUBLEBUFFER включает режим двойной буферизации, когда вывод осуществляется не на экран, а в память, затем содержимое буфера выводится на экран. Это очень полезный режим: если в любом примере на анимацию убрать режим двойной буферизации и все связанные с этим режимом команды, то при выводе кадра будет заметно мерцание. Во всех последующих примерах, начиная со второй главы, я буду использовать этот режим, кроме некоторых специально оговариваемых случаев.

Замечание
Кадр, содержимое которого мы непосредственно видим на экране, называется передним буфером кадра, вспомогательный раздел памяти, в котором подготавливается изображение, называется задним буфером кадра

Константу PFD_GENERIC_ACCELERATED имеет смысл устанавливать только в случае, если компьютер оснащен графическим акселератором.
Флаги, заканчивающиеся на "DONTCARE", сообщают системе, что соответствующий режим может иметь оба значения, например, при установке флага PFD_DouBLE_BUFFER_DONTCARE запрашиваемый формат пиксела допускает оба режима - как одинарной, так и двойной буферизации.
Со всеми остальными полями и константами я предоставляю вам возможность разобраться самостоятельно. Отмечу, что поле iLayerType, описанное в windows. pas как имеющее тип Byte, может, согласно справке, иметь три значения: PFD_MAIN_PLANE, PFD_OVERLAY__PLANE И PFD_UNDERLAY_PLANE, однако константа PFD_UNDERLAY_PLANE имеет значение -1, так что установить такое значение для величины типа Byte не удастся.
OpenGL позволяет узнать, какой же формат пиксела он собирается использовать. Для этого необходимо использовать функцию DescribePixelFormat, заполняющую величину типа TPixelFormatDescriptor установленным форматом пиксела.
Построим несложное приложение на основе использования этой функции, которое позволит детальнее разобраться с форматом пиксела и подобрать формат для конкретного рабочего места (проект из подкаталога Ex22).
примере битовым флагам задаем все возможные значения одновременно, числовым полям задаем заведомо нереальное значение 64, и смотрим на выбор формата пиксела, сделанный OpenGL. Результат, который вы получите для выбранного OpenGL формата пиксела, я предсказать не могу: он индивидуален для каждой конкретной конфигурации компьютера и текущих настроек. Скорее всего окажется, что режим двойной буферизации не будет установлен. (Напоминаю: многие флаги устанавливаются только в определенных комбинациях с другими.)Наше приложение позволяет менять параметры формата пиксела и устанавливать его заново, а чтобы видеть воспроизведение, небольшая площадка на экране при каждом тестировании окрашивается случайным цветом, используя функции OpenGL. Поэкспериментируйте с этим приложением, например, определите комбинацию флагов для установления режима двойной буферизации. Посмотрите значение числовых полей формата при различной палитре экрана: 16 бит, 24 бита, 32 бита, если у вас есть такой режим, но не в палитре с 256 цветами. О выводе OpenGL при палитре экрана в 256 цветов у нас будет отдельный разговор.
Это приложение, в частности, дает ответ на вопрос, как определить, оснащен ли компьютер графическим акселератором. Сделать это можно после вызова функции DescribePixelFormat следующим образом:

var
i, j: Integer;
i: = pfd. dwFlags and PFD_GENERIC_ACCELERATED; ]: = pfd.
dwFlags and PFD_GENERIC__FORMAT;
If (i = 0) and (з = 0)
then // полноценным ICD-драйвер с функциями ускорения
else If (i = 1) and (j = 1)
then // MCD-драйвер, аппаратно реализуется
// только часть функций ускорения
else // режим программной эмуляции, всю работу выполняет центральный
// процессор

В следующей главе мы узнаем еще один способ определения наличия акселератора.
С помощью рассмотренного проекта вы найдете ответ на вопрос, на который я вам ответить не смогу, а именно - как заполнить структуру TPixelFormatDescriptor для вашего компьютера.

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

If hrc=0 then ShowMessage('Oтсутствует контекст воспроизведения OpenGL');

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

FillChar {pfd, SizeOf (pfd), 0);
nPixelFormat: =ChoosePixelFormat (hdc, @pfd);
SetPixelFormat (hdc, nPixelFormat, @pfd);

To есть ни одно из полей pfd я не задаю явно, отдавая все на откуп OpenGL. B некоторых же примерах я ограничиваюсь только заданием необходимых значений для полей битовых флагов.
Я не встречал ситуаций, когда бы такой подход не срабатывал, не считая случаев с использованием других, нежели фирмы Microsoft, версий OpenGL, но поручиться за то, что он будет работать для всех графических карт, не могу. Возможно, проблемы возникнут также из-за некорректной работы Драйверов (стандартная отговорка, не правда ли?).
Если примеры с прилагаемой дискеты у вас не работают, выдавая просто черный экран, начните поиск причины с определения значения hrc сразу жe после создания ссылки на контекст воспроизведения. Если это значение Равно нулю, в процедуре установки формата пиксела задайте всем полям значения согласно полученным с помощью приложения проекта TestPFD.Скорее всего, вам рано или поздно потребуется разрешать подобные проблемы, связанные с неверным форматом пиксела или подобными системными ошибками. Сделать это в проектах, где не используется библиотека классов Delphi, оказывается сложным для новичков. Помощью может стать пример из подкаталога Ex23, где я демонстрирую, как выдать информацию о последней системной ошибке. В программе намеренно сделана ошибка путем превращения строки с получением ссылки на контекст устройства в комментарий.
Функция API FormatMessage позволяет преобразовать сообщение об ошибке в формат, пригодный для вывода:

lpMsgBuf: PChar;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER or
FORMAT_MESSAGE_FROM_SYSTEM,
nil, GetLastError(), LANG_NEUTRAL, @lpMsgBuf, 0, nil);
MessageBox(Window, lpMsgBuf, 'GetLastError', MB_OK);

Сообщение выводится в осмысленной форме и на языке, соответствующем локализации операционной системы. В данном случае выводится фраза "Неверный дескриптор".
Если убрать знаки комментария со строки с получением ссылки, а закомментировать строку с вызовом функции setPixelFormat, сообщение об ошибке будет выглядеть как "Неправильный формат точки" (подразумевается "Неверный формат пиксела").
Полный список системных ошибок, связанных с использованием OpenGL, можно посмотреть в файле windows. pas, в разделе "OpenGL Error Code".
Учтите, что в этом примере выводится информация о последней системной ошибке, а она могла произойти задолго до работы приложения, так что следует использовать такую диагностику ошибок только при отладке приложений. Первый аргумент функции API FormatMessage позволяет определять дополнительные детали вывода сообщения.

Замечание
Во второй главе мы познакомимся с еще одним способом диагностирования ошибок, стандартным для OpenGL.

Вывод на компоненты Delphi средствами OpenGL
Теоретически с помощью функций OpenGL можно осуществлять вывод не только на поверхность формы, но и на поверхность любого компонента, если у него имеется свойство Canvas. Handle, для чего при получении ссылки на контекст воспроизведения необходимо указывать ссылку на контекст устройства, ассоциированную с нужным компонентом, например, image1. Canvas. Handle. Однако чаще всего это приводит к неустойчивой работе, вывод то есть, то нет, хотя контекст воспроизведения присутствует и не теряется.
OpenGL прекрасно уживается с визуальными компонентами, как видно из примера TestPFD, так что чаще всего нет необходимости осуществлять вывод на поле не формы, а компонента Delphi.
Если для ваших задач необходимо ограничить размер области вывода, то для этого есть стандартные методы, которые мы обсудим во второй главе.
Подкаталог Ex24 содержит проект, в котором вывод осуществляется на поверхность панели - компонента, вообще не имеющего свойства canvas. Для этого мы пользуемся тем, что панель имеет отдельной окно:

dc: = GetDC (Panel1. Handle);
SetDCPixelFormat(dc);
hrc: = wglCreateContext(dc);

Аналогичным образом можно организовать вывод на поверхность любого компонента, имеющего свойство Handle (т. e. имеющего самостоятельное окно), например, на поверхность обычной кнопки. Обязательно попробуйте сделать это.
Для вывода на компонент класса TImage можете записать.

dc: = Image1. Canvas. Handle; и удалить строки BeginPaint и EndPaint, поскольку класс TImage не имеет

свойства Handle, т. e. не создает отдельного окна.
Однако вывод на компоненты, подобные компонентам класса Timage, т. e. не имеющие свойства Handle, отличается полной неустойчивостью, так что я не гарантирую вам надежного положительного результата.
Почему это происходит, выясним в следующем разделе.

Стили окна и вывод OpenGL
В проекте из подкаталога Ex25 я немного модифицировал пример минимальной программы OpenGL таким образом, что получилось простое MDI-приложение, в котором каждое дочернее окно окрашивается случайным образом с использованием команд OpenGL (рис. 1. 2).

Рис. 1. 2. Приложение для вывода командами OpenGL может иметь сколько угодно окон

Проект из подкаталога Ex26 отличается от предыдущего только тем что получается SDI-приложение (рис. 1. 3).

Рис. 1. 3. Для вывода командами OpenGL можно использовать любой интерфейс приложений

из этих примеров видно, что приложение может иметь сколько угодно контекстов воспроизведения. Однако следует иметь в виду, что каждое окно может иметь только один контекст воспроизведения, чем, в частности, и объясняется неустойчивый вывод на компоненты, не имеющих собственного окна. В справке по функции SetPixelFormat говорится о том, что вывод средствами OpenGL не может быть осуществлен на окно совершенно произвольного стиля, класс окна должен включать стили "ws_ClipChildren and ws_ClipSiblings". Это соответствует обычным окнам Delphi - и родительским, и дочерним. Также говорится и о том, что атрибуты класса окна не могут включать стиль "cs_PARENTDC", что является полным ответом на вопрос о неустойчивости вывода средствами OpenGL на поверхность компонентов Типа класса TImage.
В примере из подкаталога Ex27 по сравнению с проектом минимальной программы OpenGL я поменял значение свойства Borderstyle окна формы на bsNone, т. e. осуществил вывод на окно без рамки.
Я не встречал ситуации, когда бы подобные проекты с выводом на окно без рамки или MDI-приложения с выводом средствами OpenGL на дочерние окна работали некорректно или неустойчиво, независимо от текущих установок или используемого акселератора, если свойство windowstate имеет значение wsNormal.
Более вероятно возникновение проблем в случае полноэкранных приложений.

Полноэкранные приложения
Такие приложения достойны отдельного разговора в силу их особой значимости. Занять всю область экрана вам может потребоваться для того, чтобы повысить эффектность и зрелищность вашего проекта, особенно если на экране присутствует много объектов.
Прежде всего, необходимо сказать о том, что некоторые графические акселераторы поддерживают ускорение только в полноэкранном режиме и при определенных установках экрана, например, только при разрешении экрана 640x480 и цветовой палитре 16 бит на пиксел.
К сожалению, мне придется сообщить о наличии здесь некоторых проблем, впрочем, мы их успешно разрешим. Вернитесь к последнему примеру проекту из подкаталога Ex27, где вывод средствами OpenGL осуществляется на окно без рамки и области заголовка. Поменяйте свойство windowstate формы на wsMaximized, чтобы окно после запуска раскрывалось на весь экран. Запустите проект или откомпилированный модуль. Что у вас получится, я предсказать не могу, он зависит от конкретной конфигурации машины. Если на вашем компьютере все происходит в ожидаемом режиме, т. e. весь экран занят окном голубоватого цвета, вы избавлены от множества проблем, если только не собираетесь распространять свои приложения. Дело в том, что я тестировал подобные проекты на компьютерах самой различной конфигурации и обнаружил, что чаще всего результат получается обескураживающий и неприятный: окно раскрывается некорректно, не полностью занимая экран. Причем неприятности появляются именно в связи c использованием OpenGL, простые проекты вполне справляются с задачей раскрывания на весь экран. Возможно, корень проблемы в несогласованности работы драйверов (как всегда!) или ошибках операционной системы.
Однако решение проблемы оказывается совсем простым
Посмотрите пример из подкаталога Ex28. Это тот же пример, что и предыдущий, только немного измененный. Свойство windowstate окна формы Установлено в wsNormal, а обработчик события onCreate дополнился строкой:

windowState: = wsMaximized;

Теперь все работает превосходно, окно действительно раскрывается на весь экран.
Свойство FormStyle окна можно задать как fsstayOnTop, и тогда приложение будет вести себя так, как многие профессиональные игры, не позволяя переключиться на другие приложения.
Другое решение проблемы очень похоже на предыдущее. Посмотрите пример Ex29 - модифицированный пример вывода на поверхность панели. Панель занимает всю клиентскую область окна (свойство Align имеет значение alclient), а окно формы максимизировано и не имеет рамки.
Надеюсь, у вас теперь не будет неприятностей при попытке захватить всю область экрана, хотя нелегко объяснить, почему обычный метод не работает, а такой метод, по сути, ничем от него не отличающийся, работает.
Итак, полный экран отвоевывать мы научились. Теперь необходимо научиться менять программно, т. e. без перезагрузки, разрешение экрана: приложению может потребоваться другое, нежели установленное пользователем разрешение, к которому он привык в своей повседневной работе.
Проект FullScr из подкаталога Ех30 является упрощенным вариантом такой программы. После запуска приложения пользователю из списка предлагается выбрать желаемое разрешение экрана, которое устанавливается на время работы основного модуля - минимальной программы OpenGL. После окончания работы модуля возвращается первоначальное разрешение экрана.
Пример построен на использовании функции API ChangeDisplaySettings, первый аргумент которой - структура, описывающая требуемый режим. Второй аргумент - битовая комбинация констант, одна из которых задает тестирование режима без его установки.
Массив LowResModes заполняем перечислением всех возможных режимов, тестируем последовательно каждый из них и отмечаем те, тестирование для которых оказалось успешным. После пользовательского выбора действительно устанавливаем выбранный режим, а по завершению работы приложения возвращаем запомненный первоначальный режим.
Протестировав программу в различных режимах, вы можете выяснить, что не во всех из них возможно использование OpenGL, в некоторых режимах контекст воспроизведения не может быть получен.
В целом такой способ создания полноэкранного приложения я нахожу вполне удовлетворительным. При тестировании на компьютере без акселератора приложение в проблемных режимах выдавало сообщение о невозможности получения контекста, а при подключении акселераторов сообщение не появлялось, но в некоторых режимах окно и не окрашивалось. Акселераторы первых моделей могут искаженно передавать картинку в некоторых режимах.
Приведу еще один пример на полноэкранный режим работы (проект из подкаталога Ex31). Здесь для переключения в различные режимы используется DirectDraw, все необходимые модули для его использования находятся также в этом подкаталоге.
Параметры режима - ширина, высота и разрядность пиксела - задаются в виде трех чисел в командной строке.
С помощью этого приложения вы можете выяснить, что на самом деле не все режимы доступны для использования в принципе, чем и объясняется то, что в предыдущем примере не во всех режимах можно было получить контекст воспроизведения. В проблематичных режимах на экране хорошо заметно искажение изображения.
Я думаю, что описанный способ создания полноэкранного приложения вполне можно считать универсальным и достаточно надежным.
Есть еще один, очень простой, способ создания полноэкранного приложения: рисование прямо на поверхности рабочего стола. Во второй главе я приведу соответствующий пример, хотя вы уже сейчас знаете, что для этого необходимо сделать. Но этот способ может не работать с вашей картой.

Типы OpenGL
Библиотека OpenGL является переносимой по отношению к платформам, операционным системам и средам программирования.
Для обеспечения этой независимости в ней, в частности, определены собственные типы. Их префикс - "GL", например, GLint.
В каждой среде программирования в заголовочных файлах эти типы переопределяются согласно собственным типам среды. Разберем, как это делается в Delphi.
Заголовочный файл Delphi opengl. pas начинается с определения знакомого нам типа HGLRC:

type
HGLRC = THandle;

Далее следует описание всех остальных типов OpenGL, например, наиболее "ходовой" тип GLfloat соответствует типу Single:

GLfloat = Single;

Поначалу многие испытывают затруднение, когда приходится использовать "неродные" для Delphi типы. По мере накопления опыта эта неловкость быстро проходит, и я рекомендую использовать с самого начала знакомства именно типы библиотеки OpenGL, даже если вы наизусть знаете их родные Для Delphi аналоги. Наверняка вам рано или поздно придется разбираться в чужих программах или переносить свои программы в другую среду программирования или даже в другую операционную систему В атмосфере бес прерывной смены технологий, в которой мы находимся все последние годы нельзя быть уверенным в том, что какая-либо операционная система (и/или среда программирования) на долгие годы станет надежным средством воплощения наших идей Вряд ли кто-то может поручиться, что его любимая операционная система проживет еще хотя бы десяток лет и не выйдет внезапно из моды, сменившись другой, о которой вы и не слышали пару месяцев назад.
Однако вернемся к типам OpenGL He все из них удается точно перевести Например, GLclampf - вещественное число в пределах от нуля до единицы - в Delphi определен просто как single Поэтому обычно в программах устанавливают "ручную" проверку на вхождение величины такого типа в требуемый диапазон
Будьте внимательны с целыми числами помимо типа GLint имеется тип GLUin - целое без знака, соответствующее типу Cardinal
В ряду типов OpenGL особо надо сказать о типе

GLboolean = BYTEBOOL,

Соответственно, определены две константы

GL_FALSE = 0, GL_TRDE = 1,

Константы эти имеют непосредственное отношение к типу GLboolean, однако их значения, как вы понимаете, не соответствуют типу BYTEBOOL Из-за ошибки в описании типа (или определении констант) не удастся использовать стандартный для OpenGL код, поэтому вместо констант GL_FALSE и GL_TRUE будем использовать False и True, соответственно
Конечно, можно самому скорректировать описание типа, например, так

GLboolean = 0 1,

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

TGLArrayf4 = array [0 ..3] of GLFloat,
TGLArrayf3 = array [0..2] of GLFloat,
TGLArrayf4 = array [0..3] of GLint,

Это сделано, по-видимому, для удобства кодирования и повышения читабельности кода, поскольку нигде больше в этом модуле указанные типы не встречаются
Разработчикам также пришлось ввести специальные типы для указателей, например

PGLfloat = ^GLFloat,

Такого типа нет в стандартном наборе типов OpenGL библиотека изначально создавалась на языке С, синтаксис которого хорошо приспособлен к использованию указателей, поэтому во введении особого типа для них просто не было необходимости
Вообще, должен сказать, что OpenGL наиболее приспособлен для программирования на С, поэтому некоторые моменты будут вызывать некоторые неудобства при использовании Delphi (уже упоминавшаяся система справок лишь одно звено в этой цепи) Тем не менее, это не помешает нам успешно освоить программировании на Delphi c использованием этой библиотеки
Система Delphi имеет, конечно, слабые места Чересчур большой размер откомпилированных модулей - не самое значительное из них Для графических приложений крайне важна скорость работы, и здесь пальма первенства тоже не за Delphi Если приложение интенсивно использует массивы и указатели, операции с памятью и проводит много вычислительных операций, то падение скорости при использовании Delphi вместо C/C++ оказывается значительным По некоторым тестам, лучшие компиляторы C++ создают код, работающий в два раза быстрее
Однако это не должно отпугнуть вас от дальнейшего изучения использования OpenGL в проектах Delphi, поскольку здесь как раз тот случай, когда скорость работы самого приложения не так уж и важна Основную долю работы берет на себя сервер OpenGL, a приложению достается роль транслятора команд серверу, поэтому нет особых потерь производительности, если вместо языка C++ мы используем Pascal и Delphi
Конечно, для сокращения потерь производительности желательно использовать приемы объектно-ориентированного программирования, хотя я бы не сказал, что эти приемы во всех случаях приведут к заметному на глаз ускорению работы приложения
Delphi имеет свои неоспоримые достоинства - прежде всего это несравнимая ни с каким другим средством скорость разработки и компиляции Именно поэтому, а также из-за "скрытого обаяния" Delphi (вы понимаете, о чем я говорю) мы и выбрали эту замечательную систему в качестве основы Для изучения OpenGL

Тип Tcolor и цвет в OpenGL
Разберем еще одну версию нашей первой программы, использующей OpenGL - пример из подкаталога Ex32. Здесь на форму помещена кнопка, при нажатии которой появляется стандартный диалог Windows выбора цвета. После выбора окно окрашивается в выбранный цвет, для чего используются команды OpenGL. Поскольку такой прием мы активно будем применять в будущем, разберем подробно, как это делается.
Цвет, возвращаемый диалогом, хранится в свойстве color компонента класса TColorDialog. Согласно справке, значение $00FFFFFF этого свойства соответствует белому цвету, $00FF0000 - синему, $0000FF00 - зеленому, $000000FF - красному. To есть для выделения красной составляющей цвета необходимо вырезать первый слева байт, второй байт даст долю зеленого, третий - синего. Максимальное значение байта - 255, минимальное - ноль. Цвета же OpenGL располагаются в интервале от нуля до единицы.
В нашем примере я ввел пользовательскую процедуру, определяющую тройку составляющих цветов для OpenGL по заданному аргументу типа TColor:

procedure TfrmGL. ColorToGL (с: TColor; var R, G, В: GLFloat);
begin
R: = {c mod $100) / 255;
G: = ((c div $100) mod $100) / 255;
В: = (с div $10000) / 255; end;

Из аргумента вырезаются нужные байты и масштабируются в интервал [0; 1]

Замечание
Те же действия можно сделать и другим, более "продвинутым" способом:

R =(cand$FF)/255;
G. = ((c and $FFOO) shr 8) / 255,
В: = ((c and $FFOOOO) shr 16) / 255

Эта процедура используется в обработчике нажатия кнопки:

If ColorDialogl. Execute then begin
ColorToGL (ColorDialogl. Color, R, G, В);
Refresh;
end;

В примере для простоты окно перекрашивается обычным для Delphi способом - через вызов метода Refresh формы.

Подробнее о заголовочном файле opengl. pas
Вместе с Delphi версии три и выше поставляется заголовочный файл, позволяющий подключать библиотеку OpenGL к проектам Delphi. Этот файл содержит только прототипы используемых функций и процедур, сами функции и процедуры размещены в соответствующих файлах DLL.
Например, в секции interface заголовочного файла содержится следующее forward-описание использованной нами во всех предыдущих примерах процедуры:

procedure glClearColor (red, green, blue, alpha: GLclampf); stdcall;

В секции implementation модуля собственно описание процедуры выглядит так:

procedure glClearColor; external opengl32;

Служебное слово stdcall, указанное для всех процедур и функций в этом модуле, означает стандартный вызов функции или процедуры и определяет некоторые правила обмена данными между приложением и библиотекой: как передаются параметры (через регистры или стек), в каком порядке перечисляются параметры и кто, приложение или библиотека, очищает области после их использования.
Служебное слово external указывается для функций, подключаемых из библиотек. После него следует указание имени подключаемой библиотеки. Здесь opengl32 - константа, определяемая, как я отмечал раньше, в другом модуле - в windows. pas:

opengl32 = 'opengl32. dll1;

Константа, соответствующая второй используемой библиотеке, содержится в модуле opengl.

pas:
const
glu32 = 'glu32. dll';

Содержательная часть модуля opengl, соответствующая его инициализации, содержит единственную строку:

Set8087CW($133F);

В справке по этой процедуре можно прочитать, что она служит для включения/выключения исключений при проведении операций с плавающей точкой. Здесь же отмечается, что для OpenGL рекомендуется эти исключения отключать.
Разброс описаний констант и некоторых функций и процедур по разным модулям объясняется тем, напомню, что информация, относящаяся к реализации OpenGL под Windows, помещена в заголовочный файл windows pas Это логично и объяснимо, но в некоторых случаях может вызвать дополнительные проблемы, например, при использовании альтернативных заголовочных файлов или библиотек.
Далее мы встретимся с ситуациями, когда выяснится, что в стандартном модуле, поставляемом с Delphi, не окажется прототипов многих полезных команд. Там же попробуем найти объяснение этому факту.
Альтернативные заголовочные файлы, разработанные некоторыми сторонними организациями или энтузиастами, оказываются более полными в этой части и поэтому полезны в некоторых ситуациях, но из-за того, что модуль windows pas уже содержит описание некоторых процедур, связанных с OpenGL, могут возникнуть накладки.
Следующая возможная ситуация - использование других, нежели производства фирмы Microsoft, библиотек, например, версии OpenGL фирмы SGI, которую отличают более высокие скоростные характеристики по некоторым показателям. В приложении "OpenGL в Интернете" я указал адрес, по которому вы можете найти ее дистрибутив Правда, эту версию OpenGL имеет смысл использовать только на компьютерах, не оснащенных акселератором, поскольку она не может самостоятельно использовать драйверы ICD и MCD, а переадресует все вызовы в Microsoft OpenGL, чем сводятся на нет все ее достоинства В свое время SGI обещала в следующей версии устранить этот недостаток, однако планы этой корпорации по поддержке операционной системы Windows и сотрудничеству с Microsoft, по-видимому, изменились, так что эта работа, возможно, не будет завершена.
Если вам потребуется модифицировать заголовочные файлы для подключения другой библиотеки, то придется учитывать несколько нюансов, связанных с версиями используемой Delphi.
Здесь я смогу только обозначить эти тонкости Хотя я и нашел решение проблемы подключения других версий OpenGL, но не имею возможности поделиться им - файл windows pas даже в сжатом виде имеет очень большой размер, поэтому на одной дискете модифицированные версии этого файла не разместить (для всех версий Delphi).
Первое, с чего надо начать, - это изменить значения констант opengl32 и glu32 в заголовочных файлах и установить имена соответствующих файлов библиотек. Если вас не страшит то, что модифицируются стандартные модули Delphi, можете сразу компилировать проекты Если же вы модифицируете копии стандартных модулей, то придется учитывать, что почти каждый из стандартных модулей Delphi в списке uses подключает модуль windows, и, возможно, придется переставлять порядок модулей в списке uses.
Обязательно посмотрите с помощью утилит быстрого просмотра или tdump заголовочную информацию откомпилированного приложения для того, что-бы убедиться, что оно использует только те библиотеки, которые подразумевались При наличии нескольких библиотек невозможна ситуация, когда функции берутся вперемешку из разных библиотек.
Если используется только одна определенная библиотека, но контекст воспроизведения оказывается невозможным получить, попробуйте явным образом заполнять формат пиксела, указывая требуемые значения для каждого поля - это может помочь
В приложении "OpenGL в Интернете" я указал адреса, по которым вы можете получить свободно распространяемые заголовочные файлы независимых разработчиков, более полные, чем стандартный модуль Ими можно воспользоваться и в том случае, если у вас не получится самостоятельно подключать другие версии OpenGL.


OpenGL в Delphi - ГЛАВА 2 Двумерные построения

ГЛАВА 2 Двумерные построения

Точка
Команда glScissor
Совместный вывод посредством функций GDI и OpenGL
Отрезок
Треугольник
Многоугольник
Команда glEdgeFlag
Массивы вершин
Прямое обращение к пикселам экрана
Команда glGetString
Обработка ошибок
Масштабирование
Поворот
Перенос
Сохранение и восстановление текущего положения
Первые шаги в пространстве

В этой главе на самых простых примерах мы разберем азы построений объектов. Пока рассмотрим рисование только на плоскости, однако полученные знания постоянно будут использоваться при переходе к рисованию в пространстве.
OpenGL является низкоуровневой библиотекой В частности это означает, что рисование объемных фигур сводится к последовательному рисованию в пространстве плоских фигур, образующих нужную объемную. Поэтому, даже если вас интересует только использование 3D возможностей библиотеки, пропускать эту главу не рекомендуется. Примеры располагаются на дискете в каталоге Chapter2.

Точка
Для начала скажем, что в OpenGL левый нижний угол области вывода имеет координаты [-1; -1], правый верхний - [1, 1]
Начнем наше знакомство с примитивами OpenGL c самого простого из них - точки. Нарисуем на экране пять точек, четыре по углам окна и одну в центре (проект располагается в подкаталоге Ex01).
Обработчик события onPaint формы дополнился следующими строками

glViewPort (0, 0, ClientWidth, ClientHeight); // область вывода
glPointSize (20); // размер точек
glColor3f (1. 0, 1. 0, 1. 0); // цвет примитивов
glBegin (GL_POINTS); // открываем командную скобку
glVertex2f (-1, -1); // левый нижний угол
glVertex2f (-1, 1); // левый верхний угол
glVertex2f (0, 0);
glVertex2f (1, -1);
glVertex2f (1, 1); glEnd; SwapBuffers(Canvas. Handle)
// центр окна
// правый верхний угол
// правый нижний угол
// закрываем командную скобку
// содержимое буфера - на экран

Разберем каждую из строк программы подробно.
Первая строка задает область вывода указанием координат левого нижнего и правого верхнего углов (в пикселах, в оконных координатах). Я взял в качестве области вывода всю клиентскую часть окна. Если третий параметр процедуры glViewPort записать как round (clientwidth / 2), то картинка вдвое сузится, но окрашено будет все окно (проект из подкаталога Ex02).
Следующие две строки программы определяют параметры выводимых точек, размер и цвет.
На примере команды glcolor3f разберем синтаксис команд OpenGL.
Из справки по этой команде вы узнаете, что она принадлежит к целому набору команд glcolor с различными окончаниями: зb, 4i и прочие. Цифра в окончании соответствует количеству требуемых аргументов, а следующая цифрой буква показывает требуемый тип аргументов. To есть glColor3f требует в качестве аргументов тройку вещественных (float) чисел, а glColor3i - тройку целых (int) чисел.
Аналогичный синтаксис мы встретим у многих других команд OpenGL
Здесь же, в справке, выясняем, что при записи функции в вещественной форме аргументы лежат в интервале [0; 1], а в целочисленной форме - линейно отображаются на этот интервал, т. e. для задания белого цвета целочисленная форма команды будет выглядеть так:

glColor3i (2147483647, 2147483647, 2147483647); //цвет примитивов

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

Замечание
Если имя команды заканчивается на v (векторная форма), то аргументом ее служит указатель на структуру, содержащую данные, например, массив To есть, например, если последние три символа в имени команды 3fv, то ее аргумент - адрес массива трех вещественных чисел Использование такой формы команды является самым оптимальным по скоростным характеристикам.

Далее в программе следуют функции (командные скобки) glBegin и glEnd, между которыми заключены собственно процедуры рисования. Разберемся подробнее с функциями giBegin и glEnd ввиду их особой важности: большинство графических построений связано с использованием именно этой пары функций.
Во-первых, необходимо уяснить, что это командные скобки библиотеки OpenGL, не заменяющие операторные скобки языка Pascal и не имеющие к ним никакого отношения Ошибка при использовании командных скобок не распознается компилятором. Если в программе написана неправильная вложенность командных скобок OpenGL, то ошибка проявится только в процессе диалога приложения с сервером.
Во-вторых, внутри этих скобок могут находиться любые операторы языка Pascal и почти любые функции OpenGL (вернее, очень многие) Включенные в скобки команды OpenGL отрабатываются так же, как и за пределами этих скобок Главное назначение командных скобок - это задание режима (примитива) для команд glVertex (вершина), определяющих координаты вершин для рисования примитивов OpenGL.
В рассмотренном примере аргументом функции glBegin я взял символическую константу GL^POINTS Из файла справки выясняем, что в этом случае все встретившиеся до закрывающей скобки glEnd вершины (аргументы glvertex) задают координаты очередной отдельной точки. Команду glvertex я взял в форме с двумя вещественными аргументами. Получив справку по glvertex, вы можете убедиться, что и эта команда имеет целый ворох разновидностей.
Мы собирались нарисовать четыре точки по углам окна и одну в центре, поэтому между командными скобками располагаются пять строк с вызовом glvertex, аргументы которых соответствуют положениям точек в системе координат области вывода библиотеки OpenGL.
Сейчас обратите внимание на то, что вместе с изменением размеров окна рисуемое изображение также изменяется: точки всегда рисуются на своих местах относительно границ окна. Следующее замечание тоже очень важно обработчик события onResize формы тот же, что и у события OnPaint.
Приведу еще несколько простых примеров на рисование точек
Если надо нарисовать десять точек по диагонали, то можно написать так (пример располагается в подкаталоге Ех0З):

glBegin (GL_POINTS);
For i := 0 to 9 do
glVertex2f (i / 5 - 1, i / 5 - 1);
glEnd;

Ну а следующий код нарисует сотню точек со случайными координатами и цветами
  (пример из подкаталога Ех04):

glBegin (GL_POINTS);
For i := 1 to 100 do begin
glColor3f (random, random, random); glVertex2f (random * 2 - 1, random * 2-1);
end;
glEnd;

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

For i := I to 100 do begin
glColorSf (random, random, random);
glPointSize (random (20)); // обязательно за пределами скобок
glBegin (GL_POINTS);
glVertex2f (random * 2 - 1, random * 2 - 1);
glEnd;
end;

Скорее всего, вас удивило то, что точки рисуются в виде квадратиков Чтобы получить точки в виде кружочков, перед glBegin вставьте строку

glEnable (GL_POINT_SMOOTH); // включаем режим сглаживания точек

Пара команд glEnable и glDisabie играет в OpenGL очень важную роль, включая и отключая режимы работы следующих за ними команд. Нам придется еще не раз обращаться к возможным аргументам этих функций, задающим, какой конкретно режим включается или отключается.
Заканчивается обработчик события OnPaint вызовом SwapBuffers. Эта команда используется в режиме двойной буферизации для вывода на экран содержимого заднего буфера.

Замечание
Вспомним в режиме двойной буферизации все рисование осуществляется в задний буфер кадра, он является текущим на всем процессе воспроизведения По команде SwapBuffers текущее содержимое переднего буфера подменяется содержимым заднего буфера кадра, но текущим буфером после этого все равно остается задний.
Команда glciear с аргументом GL_COLOR_BUFFER_BIT очищает текущий буфер вывода.

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

Замечание
В большинстве примеров этой книги я не использую вызов glFinish или glFlush, ускорение от них получается микроскопическим. Но в программах, не применяющих двойную буферизацию, эти команды должны вызываться обязательно, иначе на экране может оказаться "недорисованная" картинка.

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

xpos: GLfloat; // координаты курсора в системе координат
OpenGL ypos: GLfloat;
Создайте следующий обработчик события MouseMove формы
xpos: = 2 * X / ClientWidth - 1;
ypos: = 2 * (ClientHeight - Y) / ClientHeight - 1;
Refresh; // перерисовываем окно

В обработчике события Paint опишите локальную целочисленную переменную i и содержательную часть кода приведите к виду

For i: = 1 to 40 do begin // сорок точек
glColor3f (random, random, random); // случайного цвета
glPointSize (random (10)); // случайного размера
glBegin (GL_POINTS}; // со случайными координатами вокруг курсора
glVertex2f (xpos + 0. 2 * random * sin (random (360)),
ypos + 0. 2 * random * cos (random (3 60)));
glEnd;
end;

Если все сделано правильно, при движении курсора по поверхности формы в районе курсора появляется облачко разноцветных точек (готовый проект я поместил в подкаталог Ex06). Разберитесь в этом примере с масштабированием значения переменных xpos и ypos и обратите внимание, что обработчик движения мыши заканчивается вызовом Refresh - принудительной перерисовкой окна при каждом движении курсора.
Предлагаю также разобрать простой пример из подкаталога Ex07 - построение синусоиды средствами OpenGL:

procedure TfrmGL. FormPaint(Sender: TObject);
const
а = -Pi; // начало интервала
b = Pi; // конец интервала
num = 200; // количество точек на интервале
var
x: GLfloat;
l: GLint; begin wglMakeCurrent(Canvas. Handle, hrc);
glViewPort (0, 0, ClientWidth, ClientHeight);
glClearColor (0. 5, 0. 5, 0. 75, 1. 0);
glClear (GL_COLOR_BUFFER_BIT);
glEnable (GL_POINT_SMOOTH);
glColor3f (1. 0, 0. 0, 0. 5);
glBegin (GL_POINTS); For i: = 0 to num do begin x: = а + i * (b - а) / num;
glVertex2f (2 * (x - а) / (b - а) - 1. 0, sin(x) * 0. 75);
end;
glEnd;
SwapBuffers(Canvas. Handle);
wglMakeCurrent(0, 0);
end;

Пример из подкаталога Ex08 демонстрирует вывод средствами OpenGL прямо на поверхность рабочего стола. Напоминаю, окно с нулевым значением дескриптора соответствует поверхности рабочего стола, чем мы и пользуемся в этом примере для получения ссылки на контекст устройства:

dc: = GetDC (0);

Для завершения работы приложения нажмите клавиши <Alt>+<F4> или <Esc>. Обратите внимание, что по завершении работы приложения перерисовываем рабочий стол:

InvalidateRect (0, nil, False);

Перерисовка здесь необходима для восстановления нормального вида рабочего стола.

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

В заключение разговора о точках приведу одно маленькое, но важное замечание: помимо константы GL_points в OpenGL имеется символическая константа GL_poiNT, использующаяся в других, нежели glBegin, командах Но компилятор Delphi, конечно, не сможет распознать ошибку, если вместо одной константы OpenGL указать другую, когда типы констант совпадают. В этом случае ошибка приведет только к тому, что не будет построено ожидаемое изображение, аварийное завершение работы приложения не произойдет.
To же самое справедливо для всех рассматриваемых далее констант - необходимо внимательно следить за синтаксисом используемых команд.

Команда glScissor
Вернемся к проекту из подкаталога Ex02, в котором область вывода задается на половину экрана. Возможно, вас этот пример не удовлетворил: хотя картинка и выводится на половину экрана, окрашивается все-таки весь экран, а иногда это нежелательно и необходимо осуществлять вывод именно в пределах некоторой части окна.
Решение может заключаться в использовании функции вырезки glscissor, определяющей прямоугольник в окне приложения, т. e. область вырезания. После того как область вырезки задана, дальнейшие команды воспроизведения могут модифицировать только пикселы, лежащие внутри области (формулировка взята из файла справки).
Для использования этой функции необходимо включить режим учета вырезки:

glEnable(GL_SCISSOR_TEST);

После использования вырезки этот режим необходимо отключить парной командой glDisable.
Разберите проект из подкаталога Ex09 (второй пример этой главы), дополненный строками включения режима вырезки и командой, задающей область вырезки:

glEnable(GL_SCISSOR_TEST}; // включаем режим использования вырезки.
glScissor(0, 0, round(ClientWidth/2), ClientHeight); // область вырезки

Функция glScissor не заменяет команды glviewport, задающей область вывода: если в этом примере область вывода распространить на весь экран, то на экране будет рисоваться половина картинки, т. e. только то, что попадает в область вырезки.

Совместный вывод посредством функций GDI и OpenGL
Возможно, вы заинтересовались вопросом, можно ли перемежать функции вывода GDI и OpenGL. Вопрос этот, конечно, носит скорее академический, чем практический характер.
Совместное использование поверхности окна возможно при условии, что канва будет доступна для вывода, т. e. в этом случае надо обязательно освобождать контексты.

Замечание:
Многое также зависит от графической карты компьютера.

Посмотрите несложный пример из подкаталога Ex10, где код перерисовки окна первого примера данной главы дополнен строками:

Canvas. Brush. Color: = clGreen;
Canvas. Ellipse (10, 10, 50, 50);

Обратите внимание, что эти строки располагаются после строки, освобождающей контекст воспроизведения. Если поставить вывод средствами GDI перед этой строкой, вывода не произойдет, а если поставить до строки, устанавливающей контекст воспроизведения, то картинка, выдаваемая OpenGL, закроет нарисованное изображение.

Отрезок
От точек перейдем к линиям. Разберем следующий возможный аргумент команды glBegin - константу GL_LINES, задающий примитив "независимый отрезок".
Для этого примитива следующие в командных скобках вершины (т. e. функции glvertex) задают попарно координаты начала и конца каждого отрезка прямой. Снова вернемся к первому примеру с точками и подправим код рисования следующим образом (готовый проект можете найти в подкаталоге Ex11):

glBegin (GL_LINES);
glVertex2f (-1, 1);
glVertex2f (1, -1);
glVertex2f (-1, -1);
glVertex2f (1, 1);
glEnd;

Рисуются два отрезка, соединяющие углы окна по диагоналям.
Для увеличения толщины отрезков перед командными скобками укажите ширину линии:

glLineWidth (2. 5);

Эта функция также должна выноситься за командные скобки
Как и у точек, у линий можно устранять ступенчатость. Исправьте код следующим образом (подкаталог Ex12):

glLineWidth (15);
glEnable (GL_LINE_SMOOTH);
glBegin (GL_LINES);
glVertex2f (-0. 7, 0. 7);

вызовом и без вызова и посмотрите результаты работы программы с

glEnable (GL_LINE_SMOOTH).

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

glBegin (GL_LINE_STRIP);
glVertex2f (-1, -1);
glVertex2f (-1, 1);
glVertex2f (1, 1);
glVertex2f (1, -1);
glEnd;

Результат - буква П по границе окна (проект из подкаталога Exl3) В примитиве, задаваемом константой GL_LINE_LOOP, также последовательно соединяются перечисляемые вершины, однако последняя вершина замыкается с самой первой. Если в предыдущем примере использовать GL_LINE_LOOP, будет построен квадрат по границе окна (подкаталог Exl4). В примерах на отрезки мы пока использовали непрерывную линию. Для рисования пунктирной линией перед командными скобками добавьте следующие строки (проект из подкаталога Exl5):

glLineStipple (1, $FOFO);
glEnable (GL_LINE_STIPPLE);

У функции glLinestipple первый аргумент - масштабный множитель, второй аргумент задает шаблон штриховки (побитовым способом). Разберем проект из подкаталога Exl6 - еще один пример на использование штриховки (рис. 2. 1).

Рис. 2. 1. Несколько готовых шаблонов штриховых линий

Пользовательская процедура drawOneLine вызывается для воспроизведения каждого отдельного отрезка:

procedure TfrmGL. drawOneLine(xl, yl, x2, y2: GLfloat);
begin glBegin(GL_LINES);
glVertex2f glVertex2f glEnd;
end;
(2 * xl / ClientWidth - 1. 0, yl (2 * x2 / ClientWidth - 1. 0, y2
/ ClientHeight - 0. 5); / ClientHeight - 0. 5);

Содержательная часть кода перерисовки окна выглядит так:

glColor3f (1. 0, 1. 0, 1. 0); // все отрезки рисуются белым
// вторая строка: рисуется 3 отрезка, все с различной штриховкой
glEnable (GL_LINE_STIPPLE);
glLineStipple (1, $0101); // точечный
drawOneLine (50. 0, 125. 0, 150. 0, 125. 0);
glLineStipple (1, $OOFF); // штрихи
drawOneLine (150. 0, 125. 0, 250. 0, 125. 0);
glLineStipple (1, $1C47); // штрих-пунктир
drawOneLine (250. 0, 125. 0, 350. 0, 125. 0);

// третья строка: рисуется три широких отрезка с той же штриховкой
glLineWidth (5. 0); // задаем ширину линии
glLineStipple (1, $0101);
drawOneLine (50. 0, 100. 0, 150. 0, 100. 0);
glLineStipple (1, $00FF);
drawOneLine (150. 0, 100. 0, 250. 0, 100. 0);
glLineStipple (1, $1C47);
drawOneLine (250. 0, 100. 0, 350. 0, 100. 0);
glLineWidth (1. 0);
// в первой строке рисуется 6 отрезков, шаблон "пунктир/точка/пунктир",
// как части одного длинного отрезка, без вызова процедуры
drawOneLine glLineStipple (1, $1C47);
glBegin (GL_LINE_STRIP);
for i: = 0 to 6 do
glVertex2f ( 2 * (50. 0 + (i * 50. 0)) / ClientWidth - 1. 0, 75. 0 / ClientHeight);
glEnd;
// Четвертая строка - аналогичный результат, нf 6 отдельных отрезков
for i: = 0 to 5 do
drawOneLine (50. 0 + i * 50. 0, 50. 0, 50. 0 + (i+l) * 50. 0, 50. 0); // пятая
  строка - рисуется один штрихпунктирный отрезок, множитель = 5 glLineStipple
  (5, $1С47);
drawOneLine (50.0, 25.0, 350.0, 25.0);

В заключение разговора по поводу линий посмотрите проект из подкаталоге Ех17 - модифицированный пример с отслеживанием позиции курсора Теперь картинка напоминает бенгальский огонь - рисуются отрезки случайного цвета, длины, штриховки:

glEnable (GL_LINE_STIPPLE) ;
For i := 1 to 100 do begin ,
glColor3f (random, random, random);
glLineStipple (random (5), random ($FFFF) ) ;
glBegin (GL_LINES);
glVertex2f (xpos, ypos) ;
glVertex2f (xpos + 0.5 * random * sin (random (360)),
ypos + 0.5 * random * cos (random (360)));
glEnd;
end;

Треугольник
Закончив с линиями, перейдем к треугольникам - примитиву, задаваемому константой GLJTRIANGLES. В этом примитиве последующие вершины берутся триплетами, тройками, по которым строится каждый отдельный треугольник.
Следующий код служит иллюстрацией рисования одного треугольника (проект из подкаталога Ех18).

glBegin (GLJTRIANGLES) ;
glVertex2f (-1, -1);
glVertex2f (-1, 1);
glVertex2f (1, 0);
glEnd;

Для рисования правильного шестиугольника из отдельных треугольников код должен выглядеть так (готовую программу можете найти в подкаталоге Ех19):

glBegin (GLJTRIANGLES) ;
For i := 0 to 5 do begin glVertex2f (0, 0);
glVertex2f (0.5 * cos (2 * Pi * i / 6), 0.5 * sin (2 * Pi * i / 6));
glVertex2f (0.5 * cos (2 * Pi * (i + 1) /6),
0.5 * sin (2 * Pi * (i + 1) / 6) ) ;
end;
glEnd;

В качестве опорных точек выбраны шесть точек на окружности.
Надеюсь, здесь не требуется дополнительных пояснений, и мы можем перейти к примитиву, задаваемому константой GL_TRIANGLE_STRIP' связанная группа треугольников. Первые три вершины образуют первый треугольник, вершины со второй по четвертую - второй треугольник, с третьей по пятую - третий и т. д.
Проект из подкаталога Ех20 нарисует флажок, образованный наложением двух треугольников (рис. 2.2):

Рис. 2.2. Флаг получается наложением Двух отдельных треугольников

glBegin (GL_TRIANGLE_STRIP) ;
glVertex2f (1,1);
glVertex2f (-1, 1);
glVertex2f (-1, -1);
glVertex2f (1, -1);
glEnd;

Рис. 2.3. Эту картинку попробуйте нарисовать самостоятельно

А сейчас для тренировки попробуйте изменить код так, чтобы была нарисована такая же картинка, как на рис. 2.3.
Попробуем поэкспериментировать с нашей программой: будем рисовать треугольники разного цвета (проект из подкаталога Ех21):

glBegin (GL_TRIANGLE_STRIP) ;
glColorSf (0.0, 0.0, 1.0);
glVertex2f (1, 1);
glVertex2f (-1, 1);
glColor3f (1. 0, 0. 0, 0. 0);
glVertex2f (-l, -1);
glVertex2f (1, -1);
glEnd;

Результат окажется неожиданным и живописным: у фигуры возникнет плавный переход синего цвета в красный (рис. 2. 4).

Рис. 2. 4. Плавный переход цвета

Вызов перед командными скобками функции glshadeModel(GL_FLAT), задающей правило тонирования, избавит от градиентного заполнения фигуры, но результат все равно будет неожиданным - оба треугольника станут красными, т. e. цвета второго из них (проект находится в подкаталоге Ex22). Ознакомившись со справкой по этой команде, мы обнаружим, что для связанных треугольников наложение цветов происходит именно по правилу старшинства цвета второго примитива. Здесь же узнаем, что по умолчанию тонирование задается плавным, как и получилось в предыдущей программе. В продолжение экспериментов код рисования приведем к следующему виду:

glBegin (GL_TRIANGLE_STRIP);
glVertex2f (random * 2 - 1, random * 2 - 1);
For i: = 0 to 9 do begin
glColor3f (random, random, random);
glVertex2f (random * 2 - 1, random * 2 - 1);
glVertex2f (random * 2 - 1, random * 2 - 1);
end;
glEnd;

Результат получается также интересный: на экране рисуются калейдоскопические картинки, некоторые из них вполне могут порадовать глаз. Пример одной из таких картинок приведен на рис. 2. 5.

Рис. 2. 5. Результат работы программы из каталога Chapter2\Ex23

Предлагаю вам создать обработчик нажатия клавиши, включающий в себя вызов функции Refresh, тогда по нажатию любой клавиши картинка будет меняться (в подкаталоге Ex23 можете взять готовый проект).
Теперь добавьте в программу вызов glshadeModel с аргументом GL_FLAT и обратите внимание, что треугольники окрашиваются попарно одинаковым цветом. Позже мы поговорим о том, по какому правилу отображаемые примитивы накладываются друг на друга, а пока просто понаблюдайте, как рисуется подобие смятой бумажной змейки (треугольники последовательно накладываются друг на друга).
рисование шестиугольника путем наложения треугольников может быть реализовано с помощью следующего кода (пример располагается в подкаталоге Ex24):

glBegin (GL_TRIANGLE_STRIP);
For i: = 0 to 6 do begin
glColor3f (random, random, random);
glVertex2f (0, 0);
glVertex2f (0. 5 * cos (2 * Pi * i / 6),
0. 5 * sin (2 * Pi * i / 6) );
end;
glEnd;

Обязательно посмотрите результат работы этой программы, а также потренируйтесь в выборе различных моделей тонирования.
Проект из подкаталога Ex25 тоже советую не пропустить: здесь находится небольшая модификация предыдущего примера. Увеличение количества опорных точек привело к симпатичному результату: рисуется окружность с подобием интерференционной картинки на поверхности компакт-диска. Картинка меняется при нажатии клавиши и при изменении размеров окна. Следующий примитив, определяемый константой GL_TRIANGLE_FAN, также задает последовательно связанные треугольники, однако фигура строится по другому принципу: первая вершина является общей для всех остальных треугольников, задаваемых перечислением вершин, т. e. треугольники связываются наподобие веера.
Для построения шестиугольника с использованием такого примитива цикл надо переписать так (проект находится в подкаталоге Ex26):

glBegin (GL_TRIANGLE_FAN);
glVertex2f (0, 0); // вершина, общая для всех треугольников
For i: = о to 6 do begin
glColor3f (random, random, random);
glVertex2f (0. 5 * cos (2 * Pi * i / 6),
0. 5 * sin (2 * Pi * i / 6) );
end;

Теперь поговорим о режимах вывода многоугольников. Для устранения ступенчатости многоугольников используется команда:

glEnable с аргументом GL_POLYGON__SMOOTH.

Если в примеры на треугольники перед командными скобками поместить строку:

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

то треугольники будут рисоваться контурно - только линии границ (проект находится в подкаталоге Ex27).

Замечание
Запомните, что команда glPolygonMode задает режим воспроизведения для всех типов многоугольников.

Ширину линий контура можно варьировать с помощью glLineWidth, пунктирные линии контура задаются командой glLinestipple. Команда glPolygonMode позволяет выводить вместо заполненных и контурных многоугольников только их вершины, если ее вторым аргументом взять константу GL_POINT (не путать с GL_POINTS!). Размеры точек вершин и наличие сглаживания у них можно задавать так же, как и для обычных точек. По умолчанию многоугольники строятся заполненными (включен режим GL_FILL).
Команда glPolygonMode заставляет обратить внимание на порядок перечисления вершин, задающий лицевую и обратную сторону рисуемых фигур. Этот порядок для рассматриваемых плоскостных построений задает пока только то, какую сторону рисуемой фигуры мы видим, что в данном случае не особо существенно, но для будущего важно хорошо разобраться в этом вопросе.
В программе из подкаталога Ex28 рисуется все тот же шестиугольник, но вершины перечислены в обратном порядке. Контурный режим, включенный для лицевой стороны вызовом:

glPolygonMode(GL_FRONT, GL_LINE);

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

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

Многоугольник
Для рисования прямоугольника на плоскости можно воспользоваться командой glRect. Это одна из версий команды glRect. Ее аргументом являются координаты двух точек - противоположных углов рисуемого прямоугольника. Посмотрите проект, располагающийся в подкаталоге Ex29 - простой пример на построение прямоугольника с использованием этой команды.

Замечание
При использовании glRect необходимо помнить, что координата по оси Z в текущей системе координат для всех вершин равна нулю.
Константа GL_QUADS задает примитив, когда перечисляемые вершины берутся по четыре и по ним строятся независимые четырехугольники.
Следующий код - иллюстрация использования этого примитива: строятся два независимых четырехугольника (взято из проекта, располагающегося в подкаталоге Ех30).

glBegin (GL_QUADS);
glColor3f (random, random, random);
glVertex2f (-0. 6, 0. 2);
glVertex2f (-0. 7, 0. 7);
glVertex2f (0. 1, 0. 65);
glVertex2f (0. 25, -0. 78);
glColor3f (random, random, random);
glVertex2f (0. 3, -0. 6);
glVertex2f (0. 45, 0. 7);
glVertex2f (0. 8, 0. 65);
glVertex2f (0. 9, -0. 8);
glEnd;

Результат работы программы иллюстрирует рис. 2. 6.

Рис. 2. 6. Для построения независимых четырехугольников используется примитив, задаваемый константой
GL QUADS

Примитив, задаваемый константой GL_QUAD_STRIP, состоит из связанных четырехугольников Первый четырехугольник формируется из вершин номер один, два, три и четыре. Второй четырехугольник в качестве опорных берет вторую, третью, пятую и четвертую вершины Ну и так далее
Если в предыдущем примере поменять константу на CL_QUAD_STRip, как это сделано в проекте из подкаталога Ex31, то изображение в окне получится таким, как на рис. 2. 7.

Рис. 2. 7. To же, что и рис 2. 6, но константа GL QUAD STRIP

Для рисования выпуклого многоугольника используется примитив GL_POLYGON. Многоугольник строится из связанных треугольников с общей вершиной, в качестве которой берется первая среди перечисляемых в командных скобках. Код для рисования шестиугольника может выглядеть так:

glBegin (GL_POLYGON);
For i: = 0 to 6 do glVertex2f (0. 5 *cos (2 * Pi * i / 6), 0. 5 * sin (2 * Pi * i / 6));
glEnd;

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

Замечание
Для воспроизведения треугольников и четырехугольников лучше не использовать примитив GL_POLYGON, в таких случаях оптимальным будет использование примитивов, специально предназначенных для этих фигур

Попробуем чуть усложнить наши построения: зададимся целью нарисовать фигуру, как на рис. 2. 8.

Рис. 2. 8. Невыпуклый многоугольник

Поскольку примитив GL_POLYGON позволяет строить только выпуклые многоугольники, построение всей фигуры разбиваем на две части, каждая из которых представляет собой выпуклый многоугольник Соответствующий проект располагается в подкаталоге ЕхЗЗ, а на рис 2 9 я пометил эти части разными цветами.

Рис. 2. 9. Фигуру для построения разбиваем на несколько частей

Если же попытаться нарисовать эту фигуру "за один присест", то картинка может получиться, например, такой, как на рис 2 10.

Рис. 2. 10. Так выглядит попытка построения фигуры с использованием одной пары командных скобок

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

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

Возможно, вы уже задались вопросом, как нарисовать фигуру с внутренним отверстием. Подкаталог Ex35 содержит проект для рисования диска, a Ex36 содержит проект, в котором внутри квадрата рисуется круглое отверстие Всю фигуру разбиваем на четыре четверти, каждая из которых - группа связанных четырехугольников. Если включить режим контурного рисования многоугольников, картинка получится такой, как на рис. 2. 11.

Рис. 2. 11. Так разбивается фигура, если требуется нарисовать внутреннее отверстие

Если вы имеете опыт работы в графических редакторах, такой подход, возможно, вас несколько разочаровал. Конечно, было бы гораздо удобнее, если бы имелась, например, функция закраски замкнутой области, как это принято в большинстве графических редакторов. Но мы имеем дело с низкоуровневой библиотекой, и она не предоставляет подобных готовых функций.

Замечание
Как мы увидим дальше, есть несколько способов решения задачи построения сложных объектов - невыпуклых многоугольников или объектов, содержащих отверстия, - однако самый быстрый (в смысле скорости воспроизведения, а не кодирования) способ состоит в том, чтобы вы сами полностью расписали алгоритм построения на основе многоугольников, а в идеале - с использованием только треугольников.

Утешением может стать то, что аппаратные возможности растут стремительно и подобные рекомендации теряют свою актуальность.
Сейчас самое время обратить ваше внимание на очень важный факт. Если в примере на отверстие включить режим сглаживания многоугольников:

glEnable (GL_POLYGON_SMOOTH);

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

Команда glEdgeFlag
Режим вывода полигонов (так мы будем иногда называть многоугольники) позволяет рисовать контуры фигур или только точки в опорных вершинах фигуры. Когда сложная фигура разбивается на части, контурный режим может испортить картинку: станет заметным поэтапное построение фигуры. В такой ситуации решение может состоять в исключении некоторых вершин из построения границы фигуры, что осуществляется вызовом команды glEdgeFlag. Аргумент этой команды имеет тип Boolean, если точнее - GLboolean, и мы в главе говорили о небольшой проблеме с этим типом OpenGL. Как оговаривается в справке, команда дает эффект только в режиме контурного или поточечного вывода многоугольников. Также специально оговаривается возможность использования этой команды внутри командных скобок.
Смысл команды следующий: вершины, указываемые после вызова команды с аргументом False, при построении границы многоугольника не учитываются, как если бы мы рисовали контур в этом месте прозрачным цветом.
Посмотрите пример, располагающийся в подкаталоге Ex37, в котором наша тестовая фигура рисуется в двух режимах: полная заливка и контурно. Код при этом выполняется один и тот же, но для того, чтобы скрыть от наблюдателя секторное разбиение фигуры, некоторые вершины заключены между строками:

glEdgeFlag (FALSE);
glEdgeFlag (TRUE);

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

Массивы вершин
мы рассмотрели все десять примитивов, имеющихся в нашем распоряжении. Код практических построений, включающих сотни и тысячи отдельных примитивов, подчас чересчур громоздок, большая часть его в таких случаях - сотни и тысячи строк с вызовом команды glVertex.
Библиотека OpenGL располагает средством сокращения кода, базирующимся на использовании массивов вершин. В массиве вершин, т. e. массиве вещественных чисел, задаются координаты опорных вершин, по которым вызовом одной команды glDrawArrays строится последовательность примитивов заданного типа.
Поскольку эта команда не входит в стандарт OpenGL и является его расширением (extension), для получения справки по ней необходимо вызвать контекстную помощь на слово glDrawArraysEXT.
У команды glDrawArrays три аргумента: тип примитива и характеристики используемого массива.
Для использования этой функции надо, как минимум, задать массив вершин, по которым будет строиться множество примитивов. Ссылка на массив вершин создается командой glvertexPointer, также являющейся расширением стандарта. Помимо массива вершин, для украшения картинки будем также использовать массив цвета вершин, ссылка на который задается командой glcolorPointer, тоже не входящей в стандарт. Прибавив окончание Ехt к именам этих команд, можно получить контекстную подсказку, по которой легко разобраться с их аргументами. Но, к сожалению, для использования массива вершин полученной информации недостаточно, необходимо еще использовать, как минимум, команду glEnableClientstate, справку по которой уже невозможно получить никакими ухищрениями. Эта команда аналогична glEnable, но применяется только в контексте массивов вершин У нее имеется парная команда - glDisableclientstate, отключающая использование массива вершин.
В заголовочном файле opengl. pas, поставляемом с Delphi, отсутствуют прототипы команд, не входящих в стандарт OpenGL, a также отсутствует описание констант, используемых такими командами, что немного затруднит наше изучение этой библиотеки.
Обратимся к проекту из подкаталога Ex38.
Массив с именем vertex содержит координаты четырех точек - углов квадрата, а в массиве цветов Colors содержатся соответствующие вершинам значения RGB:

Vertex: Array [0.. 3, 0.. 1] of GLFloat;
Colors: Array [0.. 3, 0.. 2] of GLFloat;

Код рисования выглядит так:

glVertexPointer(2, GL_FLOAT, 0, @Vertex);
glColorPointer(3, GL FLOAT, 0, @Colors);
// указатель на массив вершин
// указатель на массив цветов
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawArrays(GL_POLYGON, 0, 4);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
// массив вершин - включаем режим
// массив цветов - включаем режим
// рисование множества полигонов
// выключаем режимы (в этом
// примере не обязательно)

Значение первого аргумента команды glvertexPointer равно двум, поскольку вершины заданы указанием двух координат. To есть этот аргумент задает, по сколько вещественных чисел считывать для каждой точки.
Результат работы программы показан на рис. 2 12.

Рис. 2. 12. Иллюстрация к примеру на использование массива вершин

Для использования команд-расширений программу пришлось дополнить строками с описанием прототипов процедур:

procedure glVertexPointer (size: GLint; atype: GLenum;
stride: GLsizel; data: pointer);
stdcall;
external OpenGL32;
Procedure glColorPointer (size: GLint; atype: GLenum; stride: GLsizel;
data: pointer);
stdcall;
external OpenGL32;
procedure glDrawArrays (mode: GLenum; first: GLint; count: GLsizel);
stdcall;
external OpenGL32;
Procedure glEnableClientState (aarray: GLenum);
stdcall;
external OpenGL32;
Procedure glDisableClientState (aarray: GLenum);
stdcall;
external CpenGL32;

OpenGL32- константа, определяемая в модуле opengl. pas. Потребовалось также задать значение констант, используемых этими процедурами:

const
GL_VERTEX_ARRAY = $8074;
GL__COLOR_ARRAY = $8076;

Значения констант и информацию о типах аргументов процедур я почерпнул из заголовочных файлов сторонних разработчиков и перепроверил по содержимому файла gl. h, поставляемым с Visual C++. Информация о команде glEnableclientstate взята из описания OpenGL фирмы SGI.
Первым аргументом glDrawArrays может быть любая константа, которую допускается использовать в glBegin. Чтобы лучше разобраться с принципом построения, рекомендую посмотреть результат работы программы с использованием отличных от GL_POLYGON констант.
В примере по массиву вершин строится множество полигонов вызовом команды

glDrawArrays(GL POLYGON, 0, 4); // рисование множества полигонов

Эта команда является сокращением следующей последовательности команд (пример из подкаталога Ex39):

glBegin (GL_POLYGON);
glArrayElement(0);
glArrayElement(1);
glArrayElement(2);
glArrayElement(3);
glEnd;

Используемая здесь функция glArrayElement (также расширение стандарта) берет в качестве вершины примитива элемент массива с заданным индексом. Индекс, как видно из примера, начинается с нуля.
В продолжение этой темы разберите также проект из подкаталога Ex40 - мою адаптацию и трансляцию под Delphi программы Polygons из книги [1], результат работы которой иллюстрирует рис. 2. 13.

Рис. 2. 13. Уже сейчас вы умеете создавать высококачественные изображения

вы возможно, задаетесь вопросом, почему в заголовочном файле, поставляемом с Delphi, отсутствуют прототипы процедур, хоть и не входящих стандарт OpenGL, но документированных разработчиком библиотеки. Ответ на этот вопрос заключается, по-видимому, в том, что этот файл является трансляцией заголовочных файлов gl. h и glu. h, опубликованных SGI, и в него вошло только то, что документировано в этих файлах. Как явствует из заголовка файла, за основу положены файлы версии 1993 года.
Помимо массивов вершин и массива цветов вершин, имеется массив границ - аналог команды glEdgeFlag применительно к массивам вершин.
В этом случае необходимо использовать команду glEdgeFlagpointer. Прибавив суффикс Ехt, вы можете получить справку по этой команде. В ней, в частности, говорится, что включение режима использования массива границ осуществляется так:

glEnable (GL_EDGE_FLAG_ARRAY_EXT);

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

glEnableClientState (GL_EDGE_FLAG_ARRAY);

To же самое я могу сказать по поводу аргументов команды.
Посмотрите проект из подкаталога Ex41, где для иллюстрации используется все та же тестовая фигура, изображенная на рис. 2. 8. В этом примере фигура выводится с использованием команды glDrawArrays в двух режимах - сплошной заливкой и контурно. При втором режиме сказывается действие подключения массива границ.
В коде программы добавился прототип процедуры glEdgeFlagPointer. Обратите внимание на расхождение в описании прототипа с документацией: У прототипа два аргумента вместо трех. Если попытаться в точности следовать документации, в результате либо не используется массив границ, либо возникает сообщение об ошибке.

Прямое обращение к пикселам экрана
Библиотека OpenGL располагает командами, позволяющими осуществить непосредственный доступ к пикселам экрана. Разберем проект из подкаталога Ex42 - простой пример на вывод блока пикселов на экран вызовом двух команд:

glRasterPos2f (-0. 25, -0. 25);
glDrawPixels(ImageWidth, ImageHeight, GL_RGB, GL_UNSIGNED_BYTE, @Image);

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

const
ImageWidth = 64;
ImageHeight = 64;
...
Image : Array [0..ImageHeight-1, 0..ImageWidth - 1, 0..2] of GLUbyte;
...
{=================================================
Создание образа шахматной доски}
procedure TfrmGL.Makelmage;
var i, j : Integer;
begin For i := 0 to ImageHeight - 1 do
For j := 0 to ImageWidth - 1 do begin
If ((i and 8) = 0) xor ( (j and 8) = 0)
then begin
Image[ i ] [ j ][ 0 ] :=0; // красный
Image [ i ] [ j ][1] : = 0; // зеленый
Image[i][j][2] := 255; // синий
end
else begin
Image[ i ] [ j ][0] := 255; // красный
Image[ i ][ j ][1] := 0; // зеленый
Image[ i ] [ j ][2] := 0; // синий
end;
end;
end;

В развитие темы разберем более интересный пример (подкаталог Ех43), где выводимый массив заполняется следующим образом" создается объект класса TBitmap, в который загружается растр из bmp-файла, последовательно считываются пикселы объекта, и в соответствии со значениями цвета каждого пиксела заполняются элементы массива. Все просто, только надо обратить внимание, что ось Y битовой карты направлена вниз, поэтому элементы массива заполняются в обратном порядке:

procedure TfrmGL.Makelmage;
var
i, ] : Integer;
PixCol : TColor;
Bitmap : TBitmap;
begin
Bitmap := TBitmap.Create;
Bitmap.LoadFromFile ('Claudia.bmp');
For i := 0 to ImageHeight - 1 do For ] := 0 to ImageWidth - 1 do begin
PixCol := Bitmap.Canvas.Pixels [j, i];
// перевод цвета из TColor в цвет для команд OpenGL
Image[ImageHeight - i - 1][3][0] := PixCol and $FF;
Image[ImageHeight - i - 1] [3] [1] := (PixCol and $FFOO) shr 8;
Image[ImageHeight - i - 1][3][2] := (PixCol and $FFOOOO) shr 16;
end,
Bitmap.Free;
end;

Обратите внимание на строку, задающую выравнивание пикселов - без нее массив пикселов будет выводиться некорректно

glPixelStorel(GL_UNPACK_ALIGNMENT, 1);

Возможно, при знакомстве с файлом справки вы обратили внимание на команду giBitMap, специально предназначенную для вывода битовых массивов Растр в таком случае может быть монохромным, а не 24-битным Обычно эта команда используется для вывода текста, мы рассмотрим ее в главе 6. Выводимые массивы пикселов легко масштабируются вызовом команды giPixeizoom, аргументы которой - масштабные множители по осям X и Y. Для иллюстрации посмотрите проект из подкаталога Ех44, где нажатием клавиш 'X' и 'Y' можно управлять масштабом по соответствующим осям Обратите внимание, что при отрицательном значении множителей изображение переворачивается по соответствующей оси тот пример демонстрирует также еще одно возможное использование команды glPixelStorel, позволяющей, в частности, прокручивать изображение по вертикали или горизонтали, что в нашей программе осуществляется нажатием клавиш 'R'1 и 'P'1

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

Команда glCopyPixeis позволяет копировать часть экрана в текущей позиции задаваемой giRasterpos объекте из диалога Ех45 я воспользовался этой командой следующим образом при нажатой кнопке мыши вслед за указателем мыши перемещается копия левого нижнего угла экрана. Изображение восстанавливается при каждой перерисовке экрана.
Здесь надо обратить внимание, что, в отличие от всех остальных примеров данной главы, флаги окна не включают двойную буферизацию и что контекст воспроизведения занимается приложением сразу по началу работы и не освобождается до завершения работы. Обработка движения мыши заканчивается не перерисовкой окна, а командой glFlush.
Команда glPixelTransfer позволяет задавать режимы вывода пикселов, в частности, задавать цветовые фильтры. В примере, располагающемся в подкаталоге Ex46, нажатием клавиш 'R', 'G', 'B' можно изменить составляющую долю соответствующего цвета.

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

Команда glReadPixels позволяет считывать содержимое всего экрана или части его. Это одна из самых важных для нас команд, мы еще неоднократно будем к ней обращаться.
Для иллюстрации ее работы я подготовил проект (подкаталог Ex47), выводящий на экран простейшие стереокартинки. Одна из них показана на рис. 2. 14.

Рис. 2. 14. Если вы умеете смотреть на стереокартинки, то увидите здесь парящий квадрат. Картинка рассчитана на размеры монитора, не ручаюсь, что в книге эффект повторится

В примере реализован следующий алгоритм: экран заполняется множеством маленьких черно-белых квадратиков, создающих хаос. Затем небольшая область экрана размером 50x50 пикселов считывается в массив и последовательно копируется восемь раз, в две строки по четыре квадрата впритык. Вот фрагмент кода для первой копии:

Pixel: Array [0.. 50, 0.. 50, 0.. 2] of GLUbyte;
// считываем в массив часть образа экрана вблизи центра
glReadPixels(round(ClientWidth / 2), round(ClientHeight / 2), 50, 50,
GL_RGB, GL_UNSIGNED_BYTE, @Pixel);
glRasterPos2f (-0. 5, 0. 0); // устанавливаем точку вывода
glDrawPixels(50, 50, GL_RGB, GL_UNSIGNED_BYTE, @Pixel}; // вывод копии

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

Команда glGetString
Вы, наверное, обратили внимание, изучая справки по командам-расширениям, что приложение может получить информацию об имеющихся расширениях OpenGL с помощью команды glGetstring. Из соответствующей справки выясняем, что результат работы команды - строка типа Pchar, а аргументом может быть одна из четырех констант.
В зависимости от используемой константы строка содержит информацию о фирме-производителе или способе воспроизведения, версии OpenGL и имеющихся расширениях стандарта.
На основе использования этой функции построен пример, располагающийся в подкаталоге Ex48. Результат работы программы представлен на рис. 2. 15.

рис. 2. 15. Пример получения информации с помощью команды glGetString

При разборе программы обратите внимание на две вещи:

  • Использовать команду gLGetstring, точно так же, как и любую другую Функцию OpenGL, мы можем только тогда, когда установлен контекст воспроизведения, хоть в данном случае не осуществляется никакого воспроизведения вообще.
  • Результат работы этой функции - длинная строка, заканчивающаяся нулем, поэтому для использования в модуле Delphi конвертируем ее с помощью функции StrPas.

Этот пример дает еще одну подсказку, как распознать наличие графического акселератора: при отсутствии такового glGetstring с аргументом GL_RENDERER возвращает строку 'GDI Generic'.
В файле справки можно также прочитать, что glGetstring с аргументом GL_EXTENSIONS возвращает строку, где через пробел перечисляются поддерживаемые текущим соединением расширения.
В частности, согласно документации, командой glDrawArrays можно пользоваться, только если в расширениях присутствует GL_EXT_VERTEX_ARRAY. Это одно из спорных мест, потому что я с успехом применяю расширение массива вершин, хотя в указанном списке ссылка на его наличие отсутствует Поэтому я бы не советовал полностью доверяться возвращаемому списку Не стоит и в программах сразу же прекращать выполнение, если вы опираетесь на расширение стандарта OpenGL, которое не определяется. По-моему, в такой ситуации достаточно просто предупредить пользователя о возможных сбоях в работе программы.

Обработка ошибок
Использующее OpenGL приложение по ходу работы может выполнять некорректные действия, приводящие к ошибкам в работе Часть ошибок носит фатальный характер, ведущий к аварийному завершению работы приложения Другие могут и не приводить к такому финалу, но код воспроизведения, содержащий ошибку, не выполняется. Пример такой ошибки, который я уже приводил - неверная константа в качестве аргумента функции glBegin или неверное количество вершин для воспроизведения примитива Конечно, компилятор не может и не должен распознавать подобные ошибки, но и OpenGL не может отработать такие указания клиента В документации по OpenGL к каждой команде прикладывается раздел "Errors", содержащий указания, какие ошибки могут возникнуть при использовании этой команды.
Команда OpenGL glGetError возвращает информацию об ошибке в виде одной из семи констант Обратите внимание, что в свою очередь эта команда сама может генерировать ошибку, если вызывается внутри командных скобок OpenGL.
На использовании этой функции построен пример, располагающийся в подкаталоге Ex49 В программе из-за заведомо неверного аргумента функции giBegm генерируется ошибка библиотеки

OpenGL типа GL_INVALIDENUM

Соответствующее сообщение выводится в заголовке окна, использовать диалоговые окна в такой ситуации нежелательно, поскольку это приведет к перерисовке окна, т e снова будет сгенерирована ошибка В программе также введена пользовательская функция, возвращающая строку с описанием ошибки:

function GetError: String;
begin Case glGetError of
GL__INVALID_ENUM: Result: = 'Неверный аргумент1';
GL INVALID_VALUE: Result: = 'Неверное значение аргумента1';
GL INVALID_OPERATION: Result: = 'Неверная операция1';
GL STACK_OVERFLOW: Result: = 'Переполнение стека1';
GL STACK_UNDERFLOW: Result: = 'Потеря значимости стека'';
GL OUT OF_MEMORY: Result: = 'He хватает памяти1';
GL_NO_ERROR: Result: = 'Нет ошибок. ';
end;
end;

Масштабирование
Мы уже знаем, что границы области вывода лежат в пределах от -1 до 1 Это может привести к неудобству при подготовке изображений К счастью, OpenGL предоставляет удобное средство на этот случай - масштабирование
Разберем его на примере программы построения фигуры, показанной на рис 2. 8.Для изменения масштаба используется команда glScalef с тремя аргументами, представляющими собой масштабные множители для каждой из осей.
Например, если перед командными скобками вставим строку

glScalef (0. 5, 0. 5, 1. 0);,

то будет нарисована уменьшенная в два раза фигура (готовый проект располагается в подкаталоге Ex50).После команд рисования необходимо восстановить нормальный масштаб, т. e в данном случае добавить строку:

glScalef (2. 0, 2. 0, 1. 0);.

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

Поворот
Для поворота используется команда glRotatef с четырьмя аргументами: угол поворота, в градусах, и вектор поворота, три вещественных числа.
Для двумерных построений наиболее нагляден поворот по оси Z, чем я и пользуюсь в приводимых примерах.
В предыдущем примере перед командными скобками вставьте строку:

glRotatef (5, 0. 0, 0. 0, 1. 0);

и создайте обработчик события Keypress с единственной командой Refresh. Теперь при нажатии любой клавиши окно перерисовывается, при этом каждый раз фигура поворачивается на пять градусов по оси Z (проект из подкаталога Ex52).
Здесь надо обратить внимание на две вещи: на то, что при положительном значении компоненты вектора поворот осуществляется против часовой стрелки и то, что важно не само значение компоненты, а ее знак и равенство/неравенство ее нулю.
Хотя мы пока ограничиваемся плоскостными построениями, поворот по любой из осей сказывается на воспроизводимой картинке. Проверьте: при повороте по осям X и Y мы получаем правильную картинку в проекции с учетом поворота по осям.
Поворот часто используется при построениях, поэтому важно разобраться в нем досконально. Точно так же, как было с масштабом, поворот действует на все последующие команды воспроизведения, так что при необходимости текущее состояние восстанавливается обратным поворотом.
Например, если надо нарисовать повернутый на 45 градусов квадрат, т e. ромб, то код должен выглядеть так (готовый проект можете взять в подкаталоге Ex53):

glRotatef (45, 0. 0, 0. 0, 1. 0);
glBegin (GL_POLYGON);
glVertex2f (-0. 6, -0. 1);
glVertex2f (-0. 6, 0. 4);
glVertex2f (-0. 1, 0. 4);
glVertex2f (-0. 1, -0. 1);
glEnd; glRotatef (-45, 0. 0, 0. 0, 1. 0);

Этот пример очень занимателен и вот почему. Удалим восстановление угла поворота и запустим приложение. Увидим не ромб, а квадрат. При внимательном рассмотрении обнаружим, что квадрат был повернут дважды Произошло это потому, что сразу после появления окна на экране (функция API showWindow) происходит его перерисовка (функция API updateWindow). Если вы были внимательны, то могли заметить, что такой же эффект наблюдался и в предыдущем примере.
При выполнении операции поворота можно спросить: поворачивается изображение или точка зрения? Мы будем для ясности считать, что поворачивается именно изображение. Следующий пример пояснит это.

Замечание
Точный ответ такой все объекты в OpenGL рисуются в точке отсчета системы координат, а команда glRotate осуществляет поворот системы координат.

Нарисуем две фигуры: квадрат и ромб, причем ромб получим путем поворота квадрата. Текст программы будет такой (проект из подкаталога Ex54):

glRotatef (45, 0. 0, 0. 0, 1. 0);
glBegin (GL_POLYGON);
glVertex2f (-0. 6, -0. 1);
glVertex2f (-0. 6, 0. 4);
glVertex2f (-0. 1, 0. 4);
glVertex2f (-0. 1, -0. 1);
glEnd;
glRotatef (-45, 0. 0, 0. 0, 1. 0);
glBegin (GL_POLYGON);
glVertex2f (0. 1, -0. 1);
glVertex2f (0. 1, 0. 4);
glVertex2f (0. 6, 0. 4);
glVertex2f (0. 6, -0. 1);
glEnd;

Точно так же нам придется поступать всегда, когда на экране присутствует несколько объектов, повернутых относительно друг друга: перед рисованием очередного объекта осуществлять поворот, а после рисования - возвращать точку зрения или осуществлять следующий поворот с учетом текущего положения точки зрения.
Пожалуйста, будьте внимательны! Начинающие пользователи OpenGL пoстоянно задают вопрос, как повернуть примитив, не поворачивая остальные примитивы. Еще один раз перечитайте предыдущий абзац В заключение разговора о повороте рассмотрите проект (подкаталог Ex55) основанный на примере с диском. При нажатой кнопке или движении кур, сора происходит перерисовка окна и поворот диска на 60 градусов.
Чтобы вы могли оценить преимущества использования "низкоуровневых" приемов, окно перерисовывается в этих случаях по-разному:

procedure TfrmGL. FormKeyPress(Sender: TObject; var Key: Char);
begin
Refresh
end;
procedure TfrmGL. FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
InvalidateRect(Handle, nil, False);
end;

При нажатии кнопки хорошо видно мерцание на поверхности окна, которого не появляется при движении указателя мыши по его поверхности.

Перенос
Перенос точки зрения (системы координат) осуществляется командой glTranslatef с тремя аргументами - величинами переноса для каждой из осей.
Все сказанное по поводу восстановления точки зрения справедливо и в отношении переноса.
Рассмотрите пример из подкаталога Ex56, иллюстрирующий использование переноса системы координат. Думаю, особых пояснений здесь не потребуется, поэтому перейдем к вопросу о совместном использовании поворота и переноса - это чаще всего и используется при построениях. Программа из подкаталога Ex57 строит узор, показанный на рис. 2. 16.

Рис. 2. 17. Еще один пример на поворот и перенос

Стоит разобрать этот пример подробнее. В цикле шесть раз происходит перенос и поворот системы координат:

glTranslatef (-0. 3, 0. 3, 0. 0);
glRotatef (60, 0, 0, 1);

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

glTranslatef (0. 4, 0. 1, 0. 0);

После цикла, конечно, требуется восстановить первоначальное положение системы координат:

glTranslatef (-0. 4, -0. 1, 0. 0);

Разобравшись с этим примером, перейдите к примеру, располагающемуся в следующем подкаталоге, Ex58. Здесь строятся шесть квадратов по кругу, как показано на рис. 2. 17.

Рис. 2.17. Еще один пример на поворот и перенос

Как и все предыдущие, этот пример тоже не отличается особой сложностью. В цикле перед рисованием очередного квадрата смешаем и поворачиваем систему координат:

glTranslatef (-0. 7 * cos (Pi * i / 3), 0. 7 * sin (Pi * i / 3), 0. 0);
glRotatef (-60 * i, 0, 0, 1);

а после рисования очередного квадрата делаем обратные действия:

glRotatef (60 * i, 0, 0, 1);
glTranslatef (0. 7 * cos (Pi * i / 3), -0. 7 * sin (Pi * i / 3), 0. 0);

Все, надеюсь, просто и понятно. Здесь только надо хорошенько уяснить, что порядок манипуляций с системой координат поменять нельзя: вначале перенос, затем поворот, по окончании рисования - в обратном порядке: поворот, затем перенос. Если поменять порядок в любой из пар этих действий либо в обеих парах, будет рисоваться другая картинка - обязательно проверьте это самостоятельно.

Сохранение и восстановление текущего положения
По предыдущим примерам вы должны были очень хорошо уяснить, наcколько важно восстанавливать текущую систему координат. При каждой следующей перерисовке окна она сдвигается и поворачивается, поэтому нам приходится постоянно следить, чтобы после рисования система координат вернулась в прежнее место. Для этого мы пользовались обратными поворотами и перемещениями. Такой подход неэффективен и редко встречается в примерах и профессиональных программах, т. к. приводит к потерям скорости воспроизведения. Если вы прочитаете справку по функциям glRotatef и glTranslatef, то узнаете, что эти функции реализуются как перемножение текущей матрицы на матрицы поворота и переноса, соответственно.
Обычно в состоянии, к которому потребуется затем вернуться, запоминают значение текущей матрицы. Вместо того чтобы заново перемножать матрицы и тратить время на операции, связанные с математическими расчетами, возвращаемся сразу к запомненному значению.
Команды glPushMatrix и glPopMatrix позволяют запомнить и восстановить текущую матрицу. Эти команды оперируют со стеком, то есть, как всегда в подобных случаях, можно запоминать (проталкивать) несколько величин, а при каждом восстановлении (выталкивании) содержимое стека поднимается вверх на единицу данных.
Использование этих функций делает код более читабельным, а воспроизведение - более быстрым, поэтому во всех оставшихся примерах мы, как правило, не будем производить малоэффективные действия по обратному перемножению матриц, а будем опираться на использование рассмотренных команд.
Для большей ясности рассмотрите пример из подкаталога Ex59 - модификацию примера на шесть квадратов. В цикле перед операциями поворота и переноса запоминаем текущую систему координат (обращаемся к glpushMatrix), а после рисования очередного квадрата - восстанавливаем ее (вызываем glPopMatrix). Надеюсь, понятно, что в стек помещается и извлекается из него пять раз одна и та же матрица, причем в этой программе в стеке содержится всегда не больше одной матрицы.
Библиотека OpenGL позволяет запоминать и возвращать не только текущую систему координат, но и остальные параметры воспроизведения.
Посмотрите документацию по функции glGet, а конкретно, список модификаций этой функции, различающихся по типу аргумента в зависимости от запоминаемого параметра. Обратите внимание, что второй аргумент функции - всегда указатель на переменную соответствующего типа, поэтому все функции заканчиваются на "v". Первый аргумент - символическая константа, указывающая, какой параметр запоминаем.
Закрепим изученный материал разбором проекта из подкаталога Ex60. Рисуем три точки, первую и третью одинаковым цветом. Код следующий:

glColor3f (1. 0, 0. 0, 0. 0);
glGetFloatv (GL CURRENT COLOR, @Color);
glBegin (GL_POINTS);
glVertex2f (-0. 25, -0. 25);
glColor3f (0. 0, 0. 0, 1. 0);
glVertex2f (-0. 25, 0. 25);
glColor3f (Color [0], Color [1], Color [2]);
glVertex2f (0. 25, 0. 25);
glEnd;

Для сохранения текущего цвета используется переменная color типа TGLArrayf3 (массив трех вещественных чисел). В качестве пояснения напомню, что вторым аргументом glGet должен быть указатель на переменную, в которой сохраняется значение параметра.

Первые шаги в пространстве
Во всех предыдущих примерах для задания вершин мы использовали версию команды glvertex с указанием двух координат вершин. Третья координата, по оси Z, устанавливалась нулевой.
Для дальнейшего знакомства с OpenGL нам необходимо выяснить одну важную вещь, для чего попробуем рисовать примитивы с заданием третьей координаты, не равной нулю.
Взгляните на пример из подкаталога Ex6l. Здесь рисуется треугольник со значением координаты вершин по Z равным текущему значению переменной h. При нажатии клавиш <пробел>+<Shift> значение этой переменной увеличивается на единицу, если нажимать просто <пробел>, то значение переменной на единицу уменьшается. Значение переменной выводится в заголовке окна. После этого картинка перерисовывается.
Поработав с этим примером, вы можете обнаружить, что пока координата по оси Z не превышает по модулю единицу, треугольник виден наблюдателю. Вы можете уменьшить шаг изменения переменной h, чтобы повысить достоверность вывода, но результат окажется таким же: как только примитив выходит за пределы воображаемого куба с координатами вершин, имеющими по модулю единичное значение, примитив перестает быть видимым.
Точнее, нам становится не видна часть примитива, выходящая за пределы этого куба. В примере из подкаталога Ex62 мы манипулируем координатой только одной из вершин треугольника, остальные не перемещаются и располагаются точно на плоскости. При движении третьей вершины в пространстве мы видим только ту часть треугольника, которая помещается в куб.
Этот пример иллюстрирует еще одну важную вещь. Помимо треугольника, здесь строится квадрат. Первым строится квадрат, вторым - треугольник. Треугольник частично перекрывает квадрат так, как если бы он был нарисован сверху. Если поменять порядок рисования примитивов, квадрат рисуется "выше" треугольника.

Замечание
Итак, OpenGL воспроизводит только те части примитивов, координаты которых не превышают по модулю единицу. Примитивы с одинаковыми координатами рисуются по принципу "каждый последующий рисуется поверх предыдущего". Если вы не теряли внимание по ходу наших уроков, то должны были убедиться в правильности второго вывода, еще разбираясь с примером на связанные четырехугольники

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


OpenGL в Delphi - ГЛАВА 3 Построения в пространстве

ГЛАВА 3 Построения в пространстве

Параметры вида
Матрицы OpenGL
Буфер глубины
Источник света
Объемные объекты
Надстройки над OpenGL
Quadric-объекты библиотеки glu
Сплайны и поверхности Безье
NURBS-поверхности
Дисплейные списки
Tess-объекты
 Таймеры и потоки

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

Параметры вида
В предыдущей главе мы убедились, что объем сцены ограничен кубом с координатами точек вершин по диагоналям (-1, -1, -1) и (1, 1, 1). Начнем дальнейшее изучение с того, что увеличим объем этого пространства. Проект из подкаталога ExOl нам поможет. На сцене присутствует все тот же треугольник, одну из вершин которого можно перемещать по оси Z нажатием клавиши <пробел>, значение координаты вершины выводится в заголовке окна. Теперь мы видим треугольник целиком в пределах большего, чем раньше, объема.
Код перерисовки окна выглядит так:

wglMakeCurrent(Canvas.Handle, hrc);
glViewport(0, 0, ClientWidth, ClientHeight);
9lPushMatrix;
glFrustum (-1, 1, -1, 1, 3, 10); // задаем перспективу
glTranslatef(0.0, 0.0, -5.0); // перенос объекта по оси 1
9lClearColor (0.5, 0.5, 0.75, 1.0);
glClear (GL_COLOR_BUFFER_BIT) ;
glColor3f (1.0, 0.0, 0.5);
glBegin (GLJTRIANGLES);
glVertex3f (-1, -1, 0);
glVertex3f (-1, 1, 0) ;
glVertexSf (1, 0, h);
glEnd;
glPopMatrix;
SwapBuffers (Canvas.Handle);
wglMakeCurrent (0, 0) ;

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

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

Теперь все, что рисуется с нулевым значением координаты Z, не будет видно наблюдателю, поскольку ближнюю плоскость отсечения мы расположили на расстоянии трех единиц от глаза наблюдателя, располагающегося в точке (0, 0, 0). Поэтому перед воспроизведением треугольника смещаем систему координат на пять единиц вниз.
Треугольник удалился от наблюдателя, и его вершины располагаются уже не на границе окна, а сместились вглубь экрана.

Замечание
В главе 6 мы узнаем, как соотнести пространственные и оконные координаты, если видовые параметры заданы с помощью команды glFrustum

Переходим к следующему примеру - проекту из подкаталога Ех02 отличие от первого примера состоит в том, что команды glPushMatrix И glPopMatrix удалены, а перед вызовом команды glFrustum стоит вызов команды glboadidentity. Будем понимать это как действие "вернуться в исходное состояние". При каждой перерисовке экрана перед заданием видовых параметров это следует проделывать, иначе объем сцены будет последовательно отсекаться из предыдущего.

Замечание
Устанавливать видовые параметры не обязательно при каждой перерисовке экрана, достаточно делать это лишь при изменении размеров окна.

Это несложное соображение предваряет следующий пример - проект из подкаталога Ех03. Для повышения надежности работы приложения пользуемся явно получаемой ссылкой на контекст устройства, а не значением свойства canvas.Handle Сразу же после получения контекста воспроизведения делаем его текущим в обработчике события create формы, а непосредственно перед удалением освобождаем контекст в обработчике Destroy.
Теперь такие параметры OpenGL, как цвет фона и цвет примитивов, можно задавать единственный раз - при создании формы, а не выполнять это действие каждый раз при перерисовке экрана. В отличие от всех предыдущих проектов, в данном появляется отдельный обработчик события, связанного с изменением размеров окна. Напомню, раньше при этом событии выполнялся тот же код, что и при перерисовке окна.
Во всех оставшихся примерах, как правило, будет присутствовать отдельный обработчик события, связанного с изменением размеров окна. В этом обработчике задается область вывода и устанавливаются параметры вида, после чего окно необходимо перерисовать:

procedure TfrmGL.FormResize(Sender: TObject);
begin
glViewport (0, 0, ClientWidth, ClientHeight);
glLoadldentity;
glFrustum (-1, 1, -1, 1, 3, 10); // видовые параметры
glTranslatef (0.0, 0.0, -5.0); // начальный сдвиг системы координат
InvalidateRect(Handle, nil. False);
end;

Код, связанный с перерисовкой окна, теперь сокращается и становится более читабельным:

Procedure TfrmGL.FormPaint(Sender: TObject);
WClear (GL_COLOR_BUFFER_BIT);
9lBegin (GLJTRIANGLES) ;
glVertex3f (-1, -1, 0) ;
glVertex3f (-1, 1, 0);
glVertex3f (1, 0, h);
glEnd;
SwapBuffers(DC);
end;

Хоть мы уже и рисуем пространственные картинки, однако почувствовать трехмерность пока не могли. Попробуем нарисовать что-нибудь действительно объемное, например, куб (проект из подкаталога Ex04). Результат работы программы - на рис. 3. 1.

Рис. 3. 1. В будущем мы получим более красивые картинки, начинаем же с самых простых

Для придания трехмерности сцене поворачиваем ее по осям:

procedure TfrmGL. FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glLoadIdentity;
glFrustum (-1, 1, -1, 1, 3, 10); // задаем перспективу
// этот фрагмент нужен для придания трехмерности
glTranslatef (0. 0, 0. 0, -8. 0); // перенос объекта - ось Z
glRotatef (30. 0, 1. 0, 0. 0, 0. 0); // поворот объекта - ось X
glRotatef (70. 0, 0. 0, 1. 0, 0. 0); // поворот объекта - ось Y
InvalidateRect(Handle, nil, False);
end;

Построение куба сводится к построению шести квадратов, отдельно для каждой стороны фигуры:

glBegin (GL_QUADS);
glVertex3f (1. 0, 1. 0, 1. 0);
glVertex3f (-1. 0, 1. 0, 1. 0);
glVertex3f (-1. 0, -1. 0, 1. 0);
glVertex3f (1. 0, -1. 0, 1. 0);
glEnd;

glBegin (GLJ2UADS);
glVertex3f (1. 0, 1. 0, -1. 0);
glVertex3f (1. 0, -1. 0, -1. 0);
glVertex3f (-1. 0, -1. 0, -1. 0),
glVertex3f (-1. 0, 1. 0, -1. 0);
glEnd;

glBegin (GL_QUADS);
glVertex3f (-1. 0, 1. 0, -1. 0);
glVertex3f (-1. 0, 1. 0, -1. 0);
glVertex3f (-1. 0, -1. 0, 1. 0);
glVertex3f (-1. 0, -1.0, 1.0);
glEnd;

glBegin (GL_QUADS);
glVertex3f (1. 0, 1. 0, 1. 0);
glVertex3f (1. 0, -1. 0, 1. 0);
glVertex3f (1. 0, -1. 0, -1. 0);
glVertex3f (1. 0, 1. 0, -1. 0);
glEnd;

glBegin (GL_QUADS);
glVertex3f (-1. 0, 1. 0, -1. 0);
glVertex3f (-1. 0, 1. 0, 1. 0);
glVertex3f (1. 0, 1. 0, 1. 0);
glVertex3f (1. 0, 1. 0, -1. 0);
glEnd;

glBegin(GL_QUADS);
glVertex3f (-1. 0, -1. 0, -1. 0);
glVertex3f (1. 0, -1. 0, -1. 0);
glVertex3f (1. 0, -1. 0, 1. 0);
glVertex3f (-1. 0, -1. 0, 1. 0);
glEnd;

Код получается громоздким, согласен.

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

Я хочу предостеречь вас на случай, если вы желаете прямо сейчас потренироваться и нарисовать что-нибудь интересное. Пока что наши примеры сильно упрощены, могут быть только учебной иллюстрацией и не годятся в качестве шаблона или основы для построения более серьезных программ Получившаяся картинка действительно трехмерная, но пространственность здесь только угадывается: куб залит монотонным цветом, из-за чего плохо понятно, что же нарисовано. Сейчас это не очень важно, мы только учимся задавать видовые параметры. Для большей определенности в ближайших примерах будем рисовать каркас куба, как, например, в следующем примере - проекте из подкаталога Ex05 (рис. 3. 2).

Рис. 3. 2. Для ориентировки в пространстве будем рисовать каркасную модель куба

Чтобы выводить только ребра куба, после установления контекста воспроизведения задаем нужный режим воспроизведения полигонов:

glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);

Получившаяся картинка иллюстрирует важную вещь: использование команды glFrustum приводит к созданию перспективной проекции. Хорошо видно, что ребра куба не параллельны друг другу и имеют точку схода где-то на горизонте.
Чтобы помочь вам лучше разобраться с одной из важнейших команд библиотеки OpenGL, я написал пример, расположенный в подкаталоге Ех06. В этом примере аргументы команды - переменные:

glFrustum (vLeft, vRight, vBottom, vTop, vNear, vFar);

Нажатием на пробел можно приблизить точку зрения к сцене, если при этом удерживать клавишу <Shift>, точка зрения будет удаляться.
Чтобы приблизить точку зрения, варьируем с помощью первых четырех аргументов параметры перспективы, уменьшая объем сцены, для удаления в пространстве проводим обратную операцию. Затем заново обращаемся к обработчику Resize формы, чтобы установить параметры перспективы в обновленные значения и перерисовать окно.

If Key = VK_SPACE then begin
If ssShift in Shift then begin // нажат Shift, удаляемся
vLeft: = vLeft - 0. 1;
vRight: = vRight + 0. 1;
vBottom: = vBottom - 0. 1;
vTop: = vTop + 0. 1;
end
else begin // приближаемся
vLeft: = vLeft + 0. 1;
vRight: = vRight - 0. 1;
vBottom: = vBottom + 0. 1;
vTop: = vTop - 0. 1;
end;
FormResize(nil);
end;

Аналогично клавишами управления курсором можно манипулировать значениями каждой из этих четырех переменных в отдельности, а клавиши <Insert> и <Delete> отвечают за координаты ближней и дальней плоскостей отсечения.
Так, если чересчур близко к наблюдателю расположить заднюю плоскость отсечения, дальние ребра куба станут обрезаться (рис. 3. 3).

Рис. 3. 3. Вот что происходит с кубом, если заднюю плоскость приближать слишком близко

Еще один способ приблизить к сцене точку зрения - уменьшать значение координаты передней плоскости отсечения. Возможно, это более практичный способ, поскольку у первого способа есть один недостаток: если принижаться к сцене чересчур близко, картинка переворачивается и при дальнейшем "приближении" удаляется, оставаясь перевернутой.

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

Теперь мы перейдем к другой проекции - ортографической, параллельной. Посмотрим проект из подкаталога Ех07, результат работы которого показан на рис. 3.4.

Рис. 3.4. Великий Леонардо нашел бы эту картинку нелепой, но чертежник должен быть довольным

Отличается эта проекция от перспективной именно отсутствием перспективы. В обработчике изменения размеров окна видовые параметры задаются с помощью команды glortho:

procedure TfrmGL.FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glLoadldentity;
glOrtho (-2, 2, -2, 2, 0, 15.0)
glTranslatef (0.0, 0.0, -10.0);
glRotatef (30.0, 1.0, 0.0, 0.0)
glRotatef (60.0, 0.0, 1.0, 0.0)
// видовые параметры
// перенос системы координат по оси Z
// поворот системы координат по оси X
// поворот системы координат по оси Y
InvalidateRect (Handle, nil, False);
end;

Аргументы команды glortho имеют точно такой же смысл, что и у glFrustum, но последние два аргумента могут иметь отрицательное значение. Помимо этих двух команд, OpenGL предоставляет еще несколько возможностей установки видовых параметров, например, библиотека glu содержит команду giuOrtho2D. Эта команда имеет четыре аргумента, смысл которых такой же, как и у glortho. По своему действию она эквивалентна вызову glortho с указанием значения расстояния до ближней плоскости отсечения равным минус единице, и расстоянием до дальней плоскости отсечения равным единице.
Как при такой проекции выглядит куб из предыдущих примеров, показано на рис. 3.5, а проект находится в подкаталоге Ех08.

Рис. 3.5. Командой gluOrtho2D следует пользоваться с осторожностью, но с точки зрения скорости воспроизведения она является оптимальной

Обратите внимание, что здесь отсутствует начальный сдвиг по оси Z:

procedure TfrmGL.FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glLoadldentity;
gluOrtho2D (-2, 2, -2, 2); // задаем перспективу
glRotatef (30.0, 1.0, 0.0, 0.0); // поворот объекта - ось X
glRotatef (60.0, 0.0, 1.0, 0.0); // поворот объекта - ось Y
InvalidateRect(Handle, nil, False);
end;

Куб рисуется вокруг глаза наблюдателя и проецируется на плоскость экрана. Согласно установкам этой команды передняя и задняя части нашего куба частично обрезаются.
Следующая команда, которую мы рассмотрим, пожалуй, наиболее популярна в плане использования для первоначального задания видовых параметров. Команда gluPerspective, как ясно из ее названия, также находится в библиотеке glu. Проект примера содержится в подкаталоге Ех09, а получающаяся в результате работы программы картинка показана на рис. 3.6.

Рис. 3.6. Так работает команда gluPerspective

Смысл аргументов команды поясняется в комментариях:

Procedure TfrmGL.FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glLoadIdentity
// задаем перспективу
gluPerspective(30. 0, // угол видимости в направлении оси Y
ClientWidth / ClientHeight, // угол видимости в направлении оси X
1 0, // расстояние от наблюдателя до ближней плоскости отсечения
15. 0); // расстояние от наблюдателя до дальней плоскости отсечения
glTranslatef (0. 0, 0. 0, -10. 0); // перенос - ось Z
glRotatef (30. 0, 1. 0, 0. 0, 0. 0); // поворот-ось X
glRotatef (60. 0, 0. 0, 1. 0, 0. 0); // поворот-ось Y
InvalidateRect (Handle, nil, False);
end;

Замечание
В главе 4 мы узнаем, как соотносятся аргументы команд gluPerspective и glFrustum.

С перспективами, конечно, надо попрактиковаться. Настоятельно рекомендую разобраться с примером из подкаталога Ex10. Клавиши управления курсором позволяют манипулировать значениями первых двух аргументов команды gluPerspective. При уменьшении первого аргумента происходит приближение глаза наблюдателя к сцене, уменьшение второго аргумента приводит к тому, что сцена растягивается в поперечном направлении (рис. 3 7).

Рис. 3. 7. Изменения в видовых установках приводят к трансформации объектов на экране

Замечание
Как правило, в качестве значения второго аргумента команды gluPerspective, так называемого аспекта, задают отношение ширины и высоты области вывода.

В проекте из подкаталога Ex11 я объединил все примеры на изученные команды установки видовых параметров (рис. 3. 8).

Рис. 3. 8. Команды задания видовых параметров

Вы имеете возможность еще раз уяснить различия между этими способами подготовки сцены.Библиотека glu располагает еще одной вспомогательной командой, имеющей отношение к рассматриваемой теме - gluLookAt. У нее девять аргументов: координаты позиции глаза наблюдателя в пространстве, координаты точки, располагающейся в центре экрана, и направление вектора, задающего поворот сцены (вектор "up").
При использовании этой команды можно обойтись без начальных операций со сдвигом и поворотом системы координат. Ее работу демонстрирует проект из подкаталога Exl2 (рис. 3. 9).

Рис. 3. 9. Командой gluLookAt удобно пользоваться при перемещениях точки зрения в пространстве

При задании параметров вида ограничиваемся минимумом команд:

glLoadIdentity;
gluPerspective (50. 0, ClientWidth / ClientHeight, 2. 0, 10. 0);
gluLookAt (2.7, 2, 2.5, 0.4, 0.5, 0.5, О, О, 1);
InvalidateRect(Handle, nil, False);

На рис 3.10 показаны экранные формы, выдаваемые программой из подкаталога Ех13.

Рис 3. 10. Команду gluLookAt нужно изучить основательно

В них можно менять значения всех аргументов команды gluLookAt, и поскольку этих аргументов сравнительно много, я создал вспомогательную форму, в полях редактирования которой выводятся и задаются пользователем координаты всех девяти аргументов Рекомендую обязательно попрактиковаться с этим примером, чтобы "почувствовать" работу параметров Обратите внимание, что, как подчеркивается в документации, вектор "up" не должен быть параллельным линии, соединяющей точку зрения и контрольную точку

Матрицы OpenGL
При обсуждении команд glRotatef и glTransiatef мы уже обращали внимание на то, что их действие объясняется с позиций линейной алгебры Например, про команду glTransiatef в соответствующем файле справки говорится, что эта команда умножает текущую матрицу на матрицу переноса Аналогично glRotatef сводится к операции перемножения текущей матрицы на матрицу поворота Многие изученные нами команды, действие которых я объяснял по их функциональному смыслу, в действительности описываются с помощью базовых понятий раздела математики, изучающего вопросы, связанные с геометрическими операциями, например, такими, как проекция Надеюсь, отсутствие ссылок на линейную алгебру нисколько не помешало вам при изучении предыдущего материала Если я говорю, что команда glscale является командой масштабирования (и умалчиваю о том, что она сводится к операции перемножения матриц), то рассчитываю на то, что читателю данной информации будет вполне достаточно для успешного использования этой команды на практике
Я сознательно не включил в книгу изложение математических основ для всех подобных команд, поскольку замыслил ее прежде всего как практическое руководство по компьютерной графике Мне кажется, что многие читатели не очень хотели бы особо углубляться в дебри формул Это отнюдь не значит, что я пытаюсь принизить значение математической грамотности Просто мой опыт преподавания подсказывает, что очень многие программисты предпочитают руководства, делающие упор на практике К счастью, существует масса литературы по компьютерной графике, уделяющей много внимания математике, где подробно рассказывается об операциях перемножения матриц и прочих подобных вопросах Если вы все-таки испытаете необходимость в более основательной теоретической подготовке, найти подходящее пособие не составит большого труда. Однако и в нашей практической книжке без представления о некоторых специальных терминах не обойтись. Ограничимся самым необходимым.В OpenGL имеется несколько важных матриц Матрица модели ("modelview matrix") связана с координатами объектов Она используется для того, чтобы в пространстве построить картину как бы видимую глазу наблюдателя. Другая матрица, матрица проекций ("piojection matirx"), связана с построением проекций пространственных объектов на плоскость.Матрица проекций, имея координаты точки зрения, строит усеченные ("clip") координаты, по которым после операций, связанных с перспективой, вычисляются нормализованные координаты в системе координат устройства ("normalized device coordinates") После трансформаций, связанных с областью вывода, получаются оконные координаты.Координаты вершин в пространстве и на плоскости проекций четырехмерные, помимо обычных координат есть еще w-координата Поэтому матрица модели и матрица проекций имеют размерность 4x4. Перенос и поворот системы координат сводится к операции перемножения матриц, связанных с текущей системой координат, и матриц переноса и поворота Библиотека OpenGL располагает набором команд, связанных с этими операциями, а также имеет универсальную команду для перемножения матриц glMultMatrix При желании и наличии математической подготовки этой командой можно заменить команды переноса, поворота и ряд других Посмотрим, как это делается.
В проекте из подкаталога Ех14 команда начального переноса заменена командой glMultMatrix Для этого введен массив 4x4, хранящий матрицу переноса:

rat . Array [0. 3, 0. 3] of GLfloat,

Матрица переноса заполняется нулями, кроме элементов главной диагонали, которые должны быть единицами, и последней строки, содержащей вектор переноса В примере перемещение осуществляется только по оси Z:

mt [0, 0] := 1;
№ [1, 1] := 1;
№ [2, 2] := 1;
mt [3, 3] := 1;
mt [3, 2] := -8;

Стартовый сдвиг теперь осуществляется так:

glMultMatrixf (@mt);

Если надо вернуться к исходной позиции, текущая матрица заменяется матрицей с единицами по диагонали и равными нулю всеми остальными элементами, единичной матрицей Это и есть действие команды giLoadidentity Она является частным случаем более универсальной команды glLoadMatrix, предназначенной для замены текущей матрицы на заданную, ссылка на которую передается в качестве аргумента.
Без примера эти команды не усвоить, поэтому загляните в подкаталог Exl5 Массив mt аналогичен единичной матрице. Команда glLoadidentity отсутствует, вместо ее вызова используется явная загрузка единичной матрицы:

glLoadMatrixf (@mt);

После знакомства с матричными операциями становится яснее технический смысл команд glPushMatrix и glPopMatrix, запоминающих в стеке текущую матрицу и извлекающих ее оттуда. Такая последовательность манипуляций выполняется быстрее, чем вызов glLoadMatrix. To же самое можно сказать и по поводу glLoadidentity, т. e. единичная матрица загружается быстрее
командой glLoadidentity, чем glLoadMatrix.

Замечание
Чем реже вы пользуетесь командами манипулирования матрицами, тем быстрее будет работать программа Но учтите, что если на сцене присутствует не больше пары десятков объектов, все манипуляции с матрицами съедают один кадр из тысячи.

Узнать, какая сейчас установлена матрица, можно с помощью команды glGet. Во всех наших предыдущих примерах мы задавали параметры вида применительно к матрице, установленной по умолчанию Определим, какая это матрица.
Подкаталог Ex16содержит проект, который нам в этом поможет Обработчик Resize формы дополнен выводом в заголовке окна имени текущей матрицы:

var
wrk: GLUInt; begin
glGetIntegerv (GL_MATRIX_MODE, @wrk);
case wrk of
GL_MODELVIEW: Caption: = 'GL_MODELVIEW';
GL_PROJECTION: Caption: = 'GL_PROJECTION';
end;

Запустив проект, выясняем, что по умолчанию в OpenGL установлена матрица модели. Итак, во всех предыдущих примерах операции производились над матрицей модели. В любой момент мы можем выяснить значение этой матрицы. Проиллюстрируем примером - проектом из подкаталога Exl7. При создании формы массив 4x4 заполняется матрицей модели:

glGetFloatv (GL_MODELVIEW_MATRIX, @mt);

при выборе пункта меню появляется вспомогательное окно, в котором выводится текущая матрица модели (рис 3. 11).

Рис. 3. 11. Вывод содержимого матрицы модели

При первом появлении окна эта матрица совпадает с единичной матрицей по главной диагонали единицы, все остальные элементы равны нулю Команда glLoadidentity в этой программе также заменена явной загрузкой матрицы. В проекте имеется возможность варьировать загружаемую матрицу, по нажатию на кнопку "OK" она устанавливается в модели. Сейчас у вас имеется прекрасная возможность попрактиковаться в матричных операциях. Например, числа на главной диагонали являются масштабными множителями по осям. Поменяйте первые из этих двух чисел произвольно для масштабирования по осям X и Y.

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

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

glViewport(0, 0, ClientWidth, ClientHeight);
glMatrixMode (GL_PROJECTION);
glLoadldentity;
glFrustum (-1, I, -I, I, 3, 10);
glMatrixMode (GL_MODELVIEW);
glLoadldentity;

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

Напоминаю, что параметры вида обычно помещаются в обработчике изменения размеров окна Стартовые сдвиги и повороты обычно располагаются здесь же, а код воспроизведения сцены заключается между командами glPushMatrix И glPopMatrix. Иногда поступают иначе: код воспроизведения начинается с glLoadldentity, а далее идут стартовые трансформации и собственно код сцены

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

Сейчас мы рассмотрим пару примеров на темы предыдущей главы, которые мы не могли рассмотреть раньше из-за того, что в них используются команды задания вида. Начнем с проекта из подкаталога Ех18 - модификации классической программы, поставляемой в составе OpenGL SDK. Программа рисует точку и оси системы координат (рис. 3.12).

Рис. 3.12. Результат работы проекта Point Test

Клавишами управления курсором можно передвигать точку, нажатием клавиши 'W' размер точки можно уменьшать, если же нажимать эту клавишу вместе с <shift>, то размер точки увеличивается. Первая цифровая клавиша задает режим сглаживания точки. Обратите внимание, что при установленном режиме сглаживания размер получающегося кружочка ограничен некоторыми пределами, для точек больших размеров он не вписывается точно в квадрат точки. Если точнее, то в примере рисуется две точки - вторая рисуется зеленым цветом посередине первой. Вторая точка не изменяется в размерах и всегда выводится несглаженной, но с единичным размером. Пример можно использовать в качестве простого теста, скорость передвижения большой по размеру точки позволяет оценить возможности компьютера. Посмотрите внимательно, как в этом примере задаются видовые параметры. Здесь интересно то, что плоскости отсечения привязаны к текущим размерам окна:

glViewport(О, О, ClientWidth, ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadldentity;
gluOrtho2D(-ClientWidth/2, ClientWidth/2, -ClientHeight/2, ClientHeight/2);
glMatrixMode(GL_MODELVIEW);
glLoadldentity;

Теперь при изменении размеров окна картинка не масштабируется, а изменяются размеры видимой части пространства. Если точку поставить вблизи границы окна, при уменьшении его размеров точка пропадает, выходит за пределы видимости. В примерах предыдущей главы в такой ситуации точка перемешалась внутрь окна пропорционально его размерам.
Координаты точки хранятся в массиве трех вещественных чисел, вершина воспроизведения задается командой с окончанием "v". Аргументом в этом случае является указатель на массив:

glBegin(GL_POINTS);
glVertex3fv (@point);
glEnd;

Замечание
Напоминаю, что эта - векторная - форма команд является оптимальной по скоростным показателям

Следующий пример, проект из подкаталога Ех19, тоже может использоваться в качестве простого теста скоростных характеристик компьютера. Рисуется серия отрезков прямых, наподобие разметки циферблата часов. В начале и в конце каждого отрезка рисуются точки В примере видовые параметры опираются на конкретные числа.

glViewport(0, 0, ClientWidth, ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluOrtho2D (-175, 175, -175, 175);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity;

Поэтому при изменении размеров окна картинка масштабируется так, что вся сцена всегда вписывается в экран.
Клавиша 'W' все так же позволяет регулировать ширину линий, первые две цифровые клавиши отведены для управления двумя режимами воспроизведения: первый режим включает штриховку линий, второй задает сглаживание отрезков.
Управляя этими тремя параметрами, можно получать разнообразные картинки, например такую, как приведена на рис. 3. 13.

Рис. 3. 13. Получить такие узоры на самом деле легко

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

glLineWidth(size); // ширина отрезка
If model // режим, задающий штриховку отрезков
then glEnable(GL_LINE_STIPPLE) // использовать штриховку
else glDisable(GL_LINE_STIPPLE); // не использовать штриховку
If mode2 // режим, задающий сглаженность отрезков
then glEnable (GL_LINE_SMOOTH) // сглаживать отрезки
else glDisable(GL_LINE_SMOOTH); // не использовать сглаженность
glPushMatrix; // запоминаем систему координат
For i: = 0 to 71 do begin // цикл рисования 72 отрезков
glRotatef(5. 0, 0, 0, 1); // поворот на пять градусов

glColor3f(1. 0, 1. 0, 0. 0); //цвет отрезков - желтый
glBegin(GL_LINE_STRIP); // примитив - отрезок
glVertex3fv(@pntA); // указатель на начало отрезка
glVertex3fv(@pntB); / указатель на конец отрезка
glEnd;

glColor3f(0. 0, 1. 0, 0. 0); // цвет точек - зеленый
glBegin(GL_POINTS); // примитив - точка
glVertex3fv(@pntA); // точка в начале отрезка
glVertex3fv(@pntB); // точка в конце отрезка
glEnd;
end;
glPopMatrix; // возвращаемся в первоначальную систему координат

Буфер глубины
При создании контекста воспроизведения в число параметров формата пикселов входят
  размеры разделов памяти, предоставляемой для нужд OpenGL, или буферов. Помимо
  буферов кадра, в OpenGL присутствуют еще три буфера: буфер глубины, буфер трафарета
  и вспомогательный буфер
Для специальных нужд могут использоваться еще буфер выбора и буфер обратной
  связи, они подготавливаются пользователем по мере надобности.
Работа с буферами будет нами подробно изучена в соответствующих разделах книги.
  В этом разделе мы познакомимся с буфером глубины. Как ясно из его названия,
  он используется для передачи пространства При воспроизведении каждого пиксела
  в этот буфер записывается информация о значении координаты Z пиксела, так называемая
  оконная Z. Если на пиксел приходится несколько точек, на экран выводится точка
  с наименьшим значением этой координаты.
При пространственных построениях отказ от использования буфера глубины приводит
  к неверной передаче пространства. Посмотрим, в чем тут дело. Для удобства отладки
  я написал процедуру, строящую оси координат и помечающую оси буквами X, Y и
  Z:

Procedure Axes;
var
Color: Array [1.. 4] of GLFloat;
begin
glPushMatrix;
glGetFloatv (GL_CURRENT_COLOR, @Color),

glScalef (0. 5, 0. 5, 0. 5);

glColor3f (0, 1, 0);

glBegin (GL_LINES);
glVertex3f (0, 0, 0);
glVertex3f (3, 0, 0);
glVertex3f (0, 0, 0);
glVertex3f (0, 3, 0);
glVertex3f (0, 0, 0);
glVertex3f (0, 0, 3);
glEnd;

// буква X
glBegin (GL_LINES)
glVertex3f (3. 1,-0. 2, 0. 5);
glVertex3f (3. 1,0. 2, 0. 1);
glVertex3f (3. 1,-0. 2, 0. 1);
glVertex3f (3. 1,0. 2, 0. 5);
glEnd;

// буква Y
glBegin (GL_LINES);
glVertex3f (0. 0, 3. 1, 0. 0);
glVertex3f (0. 0, 3. 1, -0. 1);
glVertex3f (0. 0, 3. 1, 0. 0);
glVertex3f (0. 1, 3. 1, 0. 1);
glVertex3f (0. 0, 3. 1, 0. 0);
glVertex3f (-0. 1, 3. 1, 0. 1);
glEnd;

// буква Z
glBegin (GL_LINES);
glVertex3f (0. 1, -0. 1, 3. 1);
glVertex3f (-0. 1, -0. 1, 3. 1);
glVertex3f (0. 1, 0. 1, 3. 1);
glVertex3f (-0. 1, 0. 1, 3. 1);
glVertex3f (-0. 1, -0. 1, 3. 1);
glVertex3f (0. 1, 0. 1, 3. 1);
glEnd;

// Восстанавливаем значение текущего цвета
glColor3f (Color [1], Color [2], Color [3])

glPopMatrix;
end;

Обратите внимание, что оси рисуются зеленым цветом, а цветовые установки запоминаются во вспомогательном массиве, по которому они восстанавливаются в конце работы процедуры.
Оси выходят из точки (0, 0, 0) и визуализируются лишь в положительном направлении. Проект находится в подкаталоге Ex20. В качестве основы взят пример glFrustum, в который добавлен вызов вышеописанной процедуры и убрано контурное отображение куба. Результат работы программы приведен на рис. 3. 14. Видно, что пространство передается некорректно, куб полностью загораживает оси координат, хотя оси должны протыкать грани куба.

Рис. 3. 14. Без использования буфера глубины пространство сцены передается некорректно

В следующем примере, проекте из подкаталога Ex21, та же сцена выводится верно (рис. 3. 15).

Рис. 3. 15. Теперь правильно: оси протыкают грани куба

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

glEnable (GL_DEPTH_TEST); // включаем режим тестирования глубины

Код сцены начинается с очистки двух буферов: буфера кадра и буфера глубины:

glClear (GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); // + буфер глубины

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

Замечание
О содержимом буфера глубины мы будем говорить еще неоднократно

С буфером глубины связаны две команды: glDepthFunc И glDepthRange. Хоть они применяются довольно редко, представление о них иметь не помешает. Первая из этих команд задает правило, по которому происходит сравнение значения оконного Z перед выводом пиксела. По умолчанию установлено значение GL_LESS - выводить на экран точки с минимальным значением оконной Z. Остальные значения приводят чаще всего к тому, что вообще ничего не будет выведено.
Вторая команда задает распределение оконной координаты Z при переводе из нормализованных координат в оконные. На рис. 3. 16 приведен результат работы программы (проект из подкаталога Ex22), где такое распределение установлено в обратное принятому по умолчанию:

glDepthRange (1, 0);

Рис. 3. 16. В любой момент можно увидеть то, что скрыто от глаза наблюдателя

Источник света
Предыдущие примеры вряд ли могут удовлетворить кого-либо в силу своей невыразительности. Рисуемый кубик скорее угадывается, все грани покрыты монотонным цветом, за которым теряется пространство. Теперь мы подошли к тому, чтобы увеличить реализм получаемых построений.
Вот в следующем примере кубик рисуется более реалистично - рис. 3. 17, проект из подкаталога Ex23.

Рис. 3. 17. Нa сцене появился источник света

При создании окна включается источник света:

glEnable (GL_LIGHTING); // разрешаем работу с освещенностью
glEnable(GL_LIGHTO); // включаем источник света

Это минимальные действия для включения источника света. Теперь в сцене присутствует один источник света с именем 0.
При необходимости можно "установить" несколько источников, для этого точно так же используется команда glEnable, например:

glEnable (GL_LIGHT1); // включаем источник света 1

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

glBegin (GL_QUADS);
glNormal3f(0. 0, 0. 0, 1. 0);
glVertex3f(1. 0, 1. 0, 1. 0);
glVertex3f(-1. 0, 1. 0, 1. 0);
glVertex3f(-1. 0, -1. 0, 1. 0);
glVertex3f(1. 0, -1. 0, 1. 0);
glEnd;

glBegin(GL_QUADS);
glNormal3f(-1. 0, 0. 0, 0. 0);
glVertex3f(-1. 0, 1. 0, 1. 0);
glVertex3f(-1. 0, 1. 0, -1. 0);
glVertex3f{-1. 0, -1. 0, -1. 0)
glVertex3f(-1. 0,-1. 0,1. 0);
glEnd;

glBegin(GL_QUADS);
glNormal3f(0. 0, 1. 0, 0. 0);
glVertex3f(-1. 0, 1. 0, -1. 0);
glVertex3f(-1. 0, 1. 0, 1. 0);
glVertex3f(1. 0, 1. 0, 1. 0);
glVertex3f(1. 0, 1. 0, -1. 0);
glEnd;

Теперь поговорим о некоторых деталях.
выясним какое максимальное число источников света мы можем использовать. Проект из подкаталога Ex24 в заголовке окна выводит это получаемое при создании окна, с помощью команды glGet:

glGetintegerv (GL_MAX_LIGHTS, @wrk);
Caption: = intToStr (wrk);

Вектора нормалей строятся перпендикулярно каждой стороне куба. В силу того, что наш кубик строится вокруг точки (0, 0, 0), аргументы glNomal3f в данном случае совпадают с точкой пересечения диагоналей каждой грани куба. Чтобы уяснить, насколько важно верно задавать вектор нормали, посмотрите пример, располагающийся в подкаталоге Ex25.
Здесь рисуется только передняя грань кубика, тремя клавишами управления курсором можно менять координаты вектора нормали. При этом меняется вид картинки: освещенность площадки передается в соответствии с текущим значением вектора нормали.
Вектор нормали не обязательно должен исходить именно из середины площадки, достаточно того, чтобы он был параллелен действительному вектору нормали к площадке. Это иллюстрирует проект из подкаталога Ex26, где теми же клавишами можно передвигать площадку в пространстве, а вектор нормали задается единожды при создании окна:

glNormal3f(-1. 0, 0. 0, 0. 0);

Где бы ни располагалась в пространстве площадка, она освещается единообразно.

Замечание
По умолчанию источник света располагается где-то в бесконечности, поэтому освещенность площадки не меняется вместе с ее перемещением.

В примере из подкаталога Ex27 клавишей <курсор влево> задается поворот площадки в пространстве вокруг оси Y, чтобы можно было взглянуть на площадку с разных точек зрения. Если мы смотрим на заднюю сторону площадки, то видим, что она окрашивается черным цветом. В некоторых ситуациях необходимо, чтобы при таком положении точки зрения наблюдателя примитив не отображался вообще, например, при воспроизведении объектов, образующих замкнутый объем, нет необходимости тратить время на воспроизведение примитивов, заведомо нам не видимых, раз они повернуты к нам задней стороной. Запомните, как это делается, разобрав проект из подкаталога Ex28.
Здесь площадка не рисуется, если повернута к наблюдателю задней стороной. Для этого необходимо включить отсечения задних сторон многоугольников:

glEnable (GL_CULL_FACE);

Команда glcullFace позволяет задавать, какие стороны при этом подвергаются отсечению, передние или задние. Понятно, что по умолчанию предлагается отсекать задние стороны. Противоположное правило отсечения можно установить так:

glCullFace (GL_FRONT);

Объемные объекты
Подчеркну еще раз, что для объемных построений используются все те же десять изученных нами во второй главе примитивов.
В следующем примере строится объемный объект на базе тестовой фигуры, знакомой нам по первой главе (рис. 3. 18). Проект расположен в подкаталоге Ex29.

Рис. 3. 18. Теперь деталь стала объемной

Шестнадцать отдельных многоугольников образуют то, что наблюдателю представляется единым объемным объектом.
В проекте введена переменная, ответственная за режим воспроизведения детали:

mode
(POINT, LINE, FILL) = LINE;

В зависимости от значения этой переменной устанавливается режим рисования многоугольников:

case mode of
POINT: glPolygonMode (GL_FRONT_AND_BACK, GL_POINT);
LINE: glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
FILL: glPolygonMode (GL_FRONT_AND BACK, GL FILL);
end;

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

(GL COLOR MATERIAL);

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

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

Надстройки над OpenGL
Существует несколько надстроек над OpenGL, представляющих собой набор готовых команд для упрощения кодирования Стандартной надстройкой, поставляемой вместе с OpenGL, является библиотека glu, физически располагающаяся в файле glu32 dll Мы уже изучили несколько команд этой библиотеки, и в дальнейшем продолжим ее изучение. Помимо этой стандартной надстройки наиболее популярной является библиотека glut Для программистов, пишущих на языке С, эта библиотека особенно привлекательна, поскольку является независимой от операционной системы Ее применение значительно упрощает кодирование программ, поскольку вся черновая работа по созданию окна, заданию формата пиксела, получению контекстов и пр выполняется с помощью вызовов библиотечных функций Вот пример начальной части программы, где с использованием функций библиотеки glut создается окно с заданными параметрами

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(400, 400);
glutInitWindowPosition(50, 50);
glutCreateWindow(argv[0]);

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

glutSolidCube (1. 0);

Ломимо куба, модуль (и библиотека) содержит команды воспроизведения сферы, тора, конуса и некоторых правильных многогранников, таких как раэдр и додекаэдр Есть здесь и команда для рисования классического объекта для тестовых программ машинной графики - чайника. Правильные многогранники строятся как совокупность многоугольников, о том, как создаются остальные объекты этой библиотеки, мы поговорим позднее. Для получения представления о модуле DGLUT разберите несложный пример, проект из подкаталога Ех30. По выбору пользователя можно построить любой объект модуля в одном из режимов точками, линиями или сплошной поверхностью Для задания типа объекта и режима воспроизведения введены переменные типа "перечисление"

mode: (POINT, LINE, FILL) = FILL;
glutobj: (CUBE, SPHERE, CONE, TORUS, DODECAHEDRON,
ICOSAHEDRON, TETRAHEDRON, TEAPOT) = CUBE;

По умолчанию заданы значения, соответствующие кубу со сплошными гранями При воспроизведении сцены устанавливается режим и производится обращение к нужной команде

case mode of
POINT glPolygonMode (GL_FRONT_AND_BACK, GL_POINT);
LINE: glPolygonMode (GL_FRONT_ANDJ3ACK, GL_LINE);
FILL: glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
end;
case glutobj of
TEAPOT: glutSolidTeapot (1. 5);
CUBE: glutSolidCube (1. 5);
SPHERE: glutSolidSphere (1. 5, 20, 20);
CONE: glutSolidCone (0. 5, 1. 5. 20, 20);
TORUS: glutSolidTorus (0. 5, 1 5, 20, 20);
DODECAHEDRON: glutSolidDodecahedron;
ICOSAHEDRON: glutSolidIcosahedron;
TETRAHEDRON: glutSolidTetrahedron; end,

Нажимая на первые три цифровые клавиши, можно задавать режим, четвертая клавиша позволяет переключать тип объекта.

If Key = 52 then begin
inc (glutobj); // установить следующее значение
If glutobj > High (glutob]) then glutobj: = Low (glutobj);
InvalidateRect(Handle, nil, False);

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

Замечание
Чем больше эти числа, тем более гладкими получаются объекты но тем больше времени требуется для их воспроизведения Если все предыдущие рекомендации по оптимизации скоростных характеристик дают малоощутимый эффект, то детализация поверхностей является одним из самых важных факторов, влияющих на скорость воспроизведения.

Для конуса задаются радиус основания, высота и пара чисел, задающих гладкость построений
У тора параметры следующие внутренний и внешний радиусы и все те же два числа, задающих, насколько плавной будет поверхность рисуемой фигуры
В этом примере режим задается способом, знакомым нам по предыдущим примерам - командой glPolygonMode Для каркасного изображения объектов модуль располагает серией команд, аналогичных тем, что мы используем в этом примере, но с приставкой glutWire вместо glutSolid
Следующий пример показывает, как можно манипулировать объектами исходного набора, чтобы получить другие объемные фигуры В подкаталоге Ex31 содержится проект, представляющий модификацию классической программы из SDK B программе моделируется рука робота в виде двух параллелепипедов (рис 3 19).

Рис. 3. 19. В программе рукой манипулятора можно управлять

Я не стал расписывать каждый параллелепипед по шести граням, а просто задал масштаб по трем осям перед воспроизведением куба так, чтобы вытянуть его в нужную фигуру

glTranslatef (-1. 0, 0. 0, 0. 0);
glRotatef (shoulder, 0. 0, 0. 0, 1 0);
glTranslatef (1 0, 0 0, 0. 0);
glPushMatrix; // запомнить текущий масштаб
glScalef (2 0, 0. 4, 1. 0); // для вытягивания куба в параллелепипед
glutSolidCube(1. 0); // в действительности - параллелепипед
glPopMatrix; // вернуть обычный масштаб
// вторая часть руки робота
glTranslatef (1. 0, 0. 0, 0. 0);
glRotatef (elbow, 0. 0, 0. 0, 1. 0);
glTranslatef (1. 0, 0. 0, 0. 0);
glPushMatrix;
glScalef (2. 0, 0. 4, 1. 0);
glutSolidCube(1. 0);
glPopMatrix;

В программе клавишами <Home>, <End>, <Insert> и <Delete> можно задавать относительное положение "суставов" руки, эмулируя управление рукой робота Первой цифровой клавишей можно менять режим воспроизведения, клавишами управления курсором можно менять положение точки зрения наблюдателя

Quadric-объекты библиотеки glu
Библиотека glu предоставляет набор команд, без которых, в принципе, можно обойтись, но с их использованием решение многих задач сильно упрощается.
Для упрощения построений некоторых поверхностей второго порядка вводится серия команд, основой которых являются квадратичные (quadric) объекты - собственный тип этой библиотеки
Как и раньше, познакомимся с новой темой на примерах, для чего откройте проект gluDisk dpr в подкаталоге Ex32 To, что получается в результате работы программы, изображено на рис 3.20.

Рис. 3.20. Проект иллюстрирует использование quadric-объектов

Пример этот является переложением на Delphi классической программы из набора примеров OpenGL SDK Прежде всего обратите внимание, что видовые параметры задаются в зависимости от пропорций окна, чтобы при любых значениях габаритов окна сцена не выходила за его пределы:

ClientWidth <= ClientHeight
then glOrtho (0. 0, 50. 0, 0. 0, 50. 0 * ClientHeight / ClientWidth,
-1. 0, 1. 0)
else glOrtho (0. 0, 50. 0 * ClientWidth / ClientHeight, 0 0, 50 0,
-1. 0, 1. 0);

Для работы с командами библиотеки glu вводится переменная специального:

quadobj: GLUquadricObj;

При создании окна вызываем команду, создающую quadric-объект, без этого действия обращение к объекту приведет к исключениям:

quadObj = gluNewQuadric,

Режим воспроизведения объекта задается командой gluQuadricDrawstyle, первым аргументом команды указывается имя quadric-объекта По умолчанию стиль задается сплошным, так что чаще всего нет надобности вызывать эту команду Она также является аналогом команды glPolygonMode и всегда может быть ею заменена, за исключением случая использования с аргументом GLU_SILHOUETTE При установлении такого режима рисуется только граничный контур фигуры, отдельные сектора не рисуются, как это сделано при рисовании третьего диска рассматриваемого примера
Диск строится с помощью команды gluDisk Помимо имени объекта ее аргументами задаются внутренний и внешний радиусы, затем два числа, задающих число разбиений диска по оси Z и число концентрических окружностей при разбиении диска Смысл этих величин хорошо виден при каркасном режиме воспроизведения, когда в получающейся паутинке хорошо видны отдельные сектора диска
В этом примере показано, как нарисовать дугу диска, сектор, "кусок пирога" Это делается с помощью команды gluPartialDisk, первые пять napavseT-ров которой полностью аналогичны параметрам gluDisk, а остальные задают начальный угол и угол развертки Углы задаются в градусах По окончании работы память, используемую quadric-объектами, необходимо освобождать Сделать это нужно до освобождения контекста воспроизведения:

gluDeleteQuadric (quadObj); // удаление объекта
wglMakeCurrent (О, О);
wglDeleteContext(hrc) ;

Замечание
Согласно файлу справки, обращение к удаленному объекту невозможно Может быть, вы столкнетесь здесь с тем же неожиданным явлением что и я поставьте вызов этой команды сразу же за его созданием и запустите программу У меня не возникло никаких исключений, что меня удивило и озадачило Тем не менее, я все же порекомендую не забывать удалять quadric-объекты.

Библиотека glu имеет средства работы с возможными исключениями, для этого предназначена команда gluQuadricCallback Первый аргумент, как обычно, имя quadric-объекта, вторым аргументом может быть только константа GLU_ERROR, третий аргумент - адрес функции, вызываемой при исключении.
В заголовочном файле отсутствует описание константы GLU_ERROR, вместо нее можно использовать присутствующую там константу GLU_TESS_ERROR, либо самому определить такую константу.

Замечание
Точнее, описание константы не отсутствует, а закомментировано.

Без примера, конечно, не обойтись, им станет проект из подкаталога Ех30. В примере описаны процедура, которая будет вызываться в случае ошибки, и нужная константа:

procedure FNGLUError;
begin
ShowMessage ('Ошибка при работе с quadric-объектом1'),
end,
const
GLU_ERROR = GLU TESS ERROR;

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

Разу после создания quadric-объекта задается функция, вызываемая при исключениях, здесь передаем адрес пользовательской процедуры:

gluQuadricCallbackfquadObj, GLU_ERROR, @FNGLUError);

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

gluQuadricDrawStyle (quadObj, GL_LINE);

Замечание
Исключение при этом не снимается, так что после этого сообщения появляется сообщение операционной системы об аварийной ситуации Можно использовать защищенный режим, чтобы такого сообщения не появлялось.

Если при обращении к команде gluQuadricCallback в качестве адреса функции задать значение, равное nil, возникающие исключения будут сниматься, а выполнение программы будет продолжаться со строки, следующей за строкой, породившей исключение.
Помимо диска, библиотека располагает командами рисования еще некоторых объектов, например, команда gluCylinder предназначена для построения цилиндров и конусов. Параметры команды следующие: имя объекта, радиусы основания и верхней части и два числа, задающих частоту разбиения. Команда gluSphere, как ясно из ее имени, упрощает построение сферы У нее четыре аргумента, второй аргумент является радиусом сферы, остальные параметры традиционны для этой серии команд.
Все объекты библиотеки glu я собрал в одном проекте, располагающемся в подкаталоге Ex34. Этот пример аналогичен примеру на объекты библиотеки glut: по выбору пользователя рисуется объект заданного типа.
Здесь также демонстрируется действие команды gluquadricOrientation, задающей направление нормали к поверхности объекта, внутрь или наружу Еще можно выяснить, как сказывается на виде объекта работа команды gluQuadricNormals, определяющей, строятся ли нормали для каждой вершины, для всего сегмента либо вообще не строятся.
Нажимая на первые четыре цифровые клавиши, можно задавать значения параметров: режим, тип объекта, ориентацию нормалей и правило их построения.
В проекте введены соответствующие перечисления:

mode: 'POINT, LINE, FILL, SILHOUETTE) = FILL;
gluobj: (SPHERE, CONE, CYLINDER, DISK) = SPHERE;
orientation: (OUTSIDE, INSIDE) = OUTSIDE;
(NONE, FLAT, SMOOTH) = SMOOTH;

Первым аргументом всех команд является имя quadric-объекта, все возможные константы перечисляются в конструкциях case:

case mode of // режим воспроизведения
POINT: gluQuadricDrawStyle (quadObj, GLU_POINT); // точки
LINE: gluQuadricDrawStyle (quadObj, GLU_LINE); // линии
FILL: gluQuadricDrawStyle (quadObj, GLU_FILL); // сплошным
SILHOUETTE: gluQuadricDrawStyle (quadObj, GLU_SILHOUETTE); // контур
end;
case orientation of // направление нормалей
INSIDE: gluQuadricOrientation (quadObj, GLU_INSIDE); // внутрь
OUTSIDE: gluQuadricOrientation (quadObj, GLU_OUTSIDE); // наружу
end;
case normals of // правило построения нормалей
NONE: gluQuadricNormals (quadObj, GLU_NONE); // не строить
FLAT: gluQuadricNormals (quadObj, GLU_FLAT); // для сегмента
SMOOTH: gluQuadricNormals (quadObj, GLU_SMOOTH); // для каждой вершины
end;
case gluobj of// тип объекта
SPHERE: gluSphere (quadObj, 1. 5, 10, 10); // сфера
CONE: gluCylinder (quadObj, 0. 0, 1. 0, 1. 5, 10, 10); // конус
CYLINDER: gluCylinder (quadObj, 1. 0, 1. 0, 1. 5, 10, 10) // цилиндр
DISK: gluDisk (quadObj, 0. 0, 1. 5, 10, 5); // диск
end;

На рис. 3. 21 приводится одна из возможных картинок, получаемых с помощью этого проекта.

Рис. 3. 21. С помощью этого примера вы можете познакомиться со всеми quadric-объектами Первым делом

Следующие примеры этого раздела также являются переложением на Delphi классических примеров из SDK. Первым делом посмотрите проект из подкаталога Ех35 - вступительный пример к нескольким следующим проектам и одновременно простейший пример на использование объектов библиотеки glu.
Здесь просто рисуется сфера, модель звезды, наблюдатель находится над полюсом (рис. 3.22).

Рис. 3.22. Упрощенная модель звезды

Проект следующего примера располагается в подкаталоге Ех32, а экранная форма приложения приведена на рис. 3.23.
Здесь моделируется уже планетарная система, клавишами управления курсором можно задавать положение планеты относительно звезды и угол поворота ее вокруг собственной оси.

Рис. 3.23. Добавилась планета

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

На базе одного quadric-объекта можно строить сколько угодно фигур; не обязательно для каждой из них создавать собственный объект, если параметры всех их идентичны.
В этом примере на базе одного объекта рисуется две сферы:

// рисуем солнце
gluSphere (quadObj, 1.0, 15, 10);
// рисуем маленькую планету
glRotatef (year, 0.0, 1.0, 0.0);
glTranslatef (2.0, 0.0, 0.0);
glRotatef (day, 0.0, 1.0, 0.0);
gluSphere (quadObj, 0.2, 10, 10);

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

Рис. 3.24. В этом примере смотрим на систему с другой точки зрения

glPush служит хорошей иллюстрацией на использование команд glPushMatrix и glPopMatrix: солнце и планета поворачиваются по отдельно-
относительно базовой системы координат:

glPushMatnx;
//Рисуем солнце
glpPushMatrix;
glRotatef (90. 0, 1. 0, 0. 0, 0. 0); // поворачиваем прямо
gluSphere (quadObj, 1. 0, 15, 10);
glPopMatrix;
// рисуем маленькую планету
glRotatef (year, 0. 0, 1. 0, 0. 0);
glTranslatef (2. 0, 0. 0, 0. 0);
glRotatef (day, 0. 0, 1. 0, 0. 0);
glRotatef (90. 0, 1. 0, 0. 0, 0. 0); // поворачиваем прямо
gluSphere (quadObj, 0. 2, 10, 10);
glPopMatrix;

Рано или поздно вам потребуется узнать, как в OpenGL можно получить вырезку пространственных фигур, например, полусферу. Следующий пример (подкаталог Ex38) поможет узнать, как это делается. В нем рисуется четверть сферы - рис. 3. 25

.

Рис. 3. 25. Совсем несложно получить полусферу или четверть сферы

Для вырезки части пространства используется новая для нас команда glclipplane. Для вырезки можно использовать несколько плоскостей, эта команда идентифицирует используемые плоскости. Первый аргумент -символическое имя плоскости вырезки, второй - адрес массива, задающего эту плоскость. Символические имена начинаются с CL__CLIP_PLANE, дальше следует цифра, нумерация начинается с нуля.
При каждой вырезке отсекается полупространство, массив задает вектор, определяющий остающуюся часть. Вектор не должен быть обязательно перпендикулярен осям, как в нашем примере.
Для получения четверти сферы проделываем две вырезки: сначала обрезаем нижнее полупространство, удаляя все вершины с отрицательным значением координаты Y, затем отсекаем левое полупространство, т. e. удаляются вершины с отрицательным значением координаты X:

const eqn: Array [0.. 3] of GLdouble = (0. 0, 1. 0, 0. 0, 0. 0);
eqn2: Array [0.. 3] of GLdouble = (1. 0, 0. 0, 0. 0, 0. 0);
// удаление нижней половины, для у < 0
glClipPlane (GL_CLIP_PLANEO, @eqn); // идентифицируем плоскость отсечения
glEnable (GL_CLIP_PLANEO); // включаем первую плоскость отсечения
// удаление левой половины, для x < 0
glClipPlane (GL_CLIP_PLANE1, @eqn2);
glEnable (GL_CLIP_PLANE1); // включаем вторую плоскость отсечения

Если теперь вернуться к библиотеке glut, то можно заметить, что сфера и конус в ней строятся на базе объектов библиотеки glu. Например, процедура для построения каркаса сферы выглядит так:

procedure glutWireSphere (
Radius: GLdouble;
Slices: GLint;
Stacks: GLint);
begin { glutWireSphere }
if quadObj = nil then
quadObj: = gluNewQuadric;
gluQuadricDrawStyle(quadObj, GLU_LINE);
gluQuadricNormals(quadObj, GLU_SMOOTH);
gluSphere(quadObj, Radius, Slices, Stacks);
end;
{ glutWireSphere }

Здесь можно подсмотреть несложный прием для определения того, надо ли создавать quadric-объект. Прием основан на том, что тип GLUquadricObj является указателем и его nil-значение соответствует тому, что объект пока еще не создан. Кстати, можете сейчас заглянуть в заголовочный файл opengl. pas, чтобы убедиться, что этот тип является указателем, указателем на пустую запись:

_GLUquadricObj = record end;
GLUquadricObj = ^_GLUquadricObj;

Заключительным примером раздела станет проект из подкаталога Ex39 - модель автомобиля (рис. 3. 26).

Рис. 3. 26. Мы можем строить модели и посложнее двух сфер

Клавишами управления курсором можно вращать модель по осям, клавиши >Insert> и <Delete> служат для приближения/удаления модели. Пробелом и клавишей ввода можно задавать различные режимы воспроизведения.В примере используются объекты библиотеки glu и модуля DGLUT. Нет смысла его подробно разбирать, остановимся только на некоторых моментах. напоминаю, что для воспроизведения объектов с различным пространственным положением перед собственно воспроизведением каждого из них система координат перемещается в нужную точку в пространстве. Для удобства ориентирования в пространстве перед каждой трансформацией системы координат текущую матрицу (систему координат) запоминаем вызовом команды glPushMatrix, затем возвращаемся в эту систему координат вызовом glPopMatrix. Это будет выполняться быстрее, чем обратные переносы но было бы лучше вычислять координаты следующего объекта в текущей системе координат.Для разнообразия передние квадратные фары автомобиля строятся с использованием команды gluDisk: если последние два аргумента команды задать равными четырем, строится ромб, перед построением система координат поворачивается:

glRotatef(45, 0. 0, 0. 0, 1. 0);
gluDisk (quadObj, 0. 0, 0. 1, 4, 4);

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

glRotatef(180, 1. 0, 0. 0, 0. 0); glRectf (0. 1, 0. 1, -0. 1, -0. 1);

Вместо этого можно было бы просто задать нужный вектор нормали:

glNormal3f (0, 0, -1);

Эту программу вы можете взять в качестве шаблона, чтобы поупражняться построениях. Проектировать системы из множества объектов без использования редакторов может оказаться трудным делом для новичков. Могу посоветовать воспользоваться приведенной выше процедурой построения осей текущей системы координат: обращайтесь к ней каждый раз, когда необходимо выяснить, "где я сейчас нахожусь".

Сплайны и поверхности Безье
Мы теперь можем без труда рисовать кубики, сферы, цилиндры и многие другие аналогичные фигуры, но, конечно, этого недостаточно, и рано или поздно возникнет потребность нарисовать поверхность произвольной формы. В этом разделе мы узнаем, как это сделать.
Подход, предлагаемый библиотекой OpenGL для изображения криволинейных поверхностей, традиционен для компьютерной графики: задаются координаты небольшого числа опорных точек, определяющих вид искомой поверхности. В зависимости от способа расчета опорные точки могут лежать на получаемой поверхности, а могут и не располагаться на ней.
Сглаживающие поверхности называются сплайнами. Есть много способов построения сплайнов, из наиболее распространенных нас будут интересовать только два: кривые Безье (Bezier curves, в некоторых книгах называются "сплайны Безье") и В-сплайны (base-splines, базовые сплайны). Изучение этой темы начнем с простейшего, с двумерных кривых.
Операционная система располагает функциями GDI, позволяющими строить кривые Безье.
В примере - проекте из подкаталога Ex40 - модуль OpenGL не используется, это пример как раз на использование функций GDI. Он является модификацией одного из проектов первой главы, но теперь рисуется не квадрат и круг, а кривая Безье по четырем точкам - рис. 3. 27.

Рис. 3. 27. Функции GDI позволяют строить кривые Безье

вершины заданы в массиве четырех величин типа Tpoint, этот тип в модуле windows. pas:

Const
Points: Array [0.. 3] of TPoint =
((x: 5; y: 5), (x: 20; y: 70), (x: 80; y: 15), (x: 100; y: 90));

Собственно рисование кривой состоит в вызове функции GDI polyBezier, первый аргумент которой - ссылка на контекст устройства, затем указывается массив опорных точек, последний аргумент - количество используемых точек:

PolyBezier (dc, Points, 4);

Построение для большей наглядности дополнено циклом, в котором окружностями визуализируются опорные точки:

For i: = 0 to 3 do
Ellipse (dc, Points [i]. x - 3, Points [i]. y - 3,
Points [i]. x + 3, Points [i]. y + 3);

В случае четырех опорных точек кривая всегда будет начинаться точно в первой точке и приходить в последнюю точку. Если их переставить местами, вид кривой не изменится, что не произойдет и если переставить местами вторую и третью опорные точки. Опорные точки могут располагаться в пространстве произвольно, т e. не требуется, чтобы они, например, равномерно располагались по интервалу. Если необходимо продолжить кривую, добавляется по три точки для каждого последующего сегмента.
Теперь посмотрим, как нарисовать кривую Безье, используя команды OpenGL. Откройте проект из подкаталога Ex41. Получаемая кривая показана на рис. 3. 28.

Рис. 3. 28. Кривая Безье, построенная с помощью функций библиотеки OpenGL

В обработчике создания формы задаются параметры так называемого одномерного вычислителя, и включается этот самый вычислитель:

glMaplf (GL_MAPl_VERTEX_3, 0. 0, 1. 0, 3, 4, @ctrlpoints);
glEnable (GL_MAPl_VERTEX_3);

Первый параметр команды glMapl - символическая константа, значение GL_MAPI_VERTEX_3 соответствует случаю, когда каждая контрольная точка представляет собой набор трех вещественных чисел одинарной точности, т. e. координаты точки Значения второго и третьего аргументов команды определяют конечные точки интервала предварительного образа рассчитываемой кривой. Величины ноль и один для них являются обычно используемыми, подробнее мы рассмотрим эти аргументы чуть ниже.
Четвертый параметр команды, "большой шаг", задает, сколько чисел содержится в считываемой порции данных. Как говорится в документации, контрольные точки могут содержаться в произвольных структурах данных, лишь бы их значения располагались в памяти друг за другом.
Последние два параметра команды - число опорных точек и указатель на массив опорных точек.
Для построения кривой можно использовать точки или отрезки' вместо команды, задающей вершину, вызывается команда glEvalcoord, возвращающая координаты рассчитанной кривой"

glBegin(GL__LINE_STRIP);
For i: = 0 to 30 do
glEvalCoordlf (i / 30. 0);
glEnd;

Аргумент команды - значение координаты u. В данном примере соединяются отрезками тридцать точек, равномерно расположенных на кривой Теперь выясним правила отображения интервала Если в этом примере третий параметр команды glMaplf задать равным двум, то на экране получим половину первоначальной кривой. Для получения полной кривой надо при воспроизведении взять интервал в два раза больший:

glBegin(GL_LINE_STRIP); For i: = 0 to 60 do
glEvalCoordlf(i / 30. 0); glEnd;

Если же этот параметр задать равным 0. 5, то при отображении интервала с u в пределах от нуля до единицы получаемая кривая не будет останавливаться на последней опорной точке, а экстраполироваться дальше. Если в этом нет необходимости, конечное значение параметра цикла надо взять равным 15 Чтобы действительно разобраться в подобных вопросах, надо обязательно попрактиковаться, посмотреть, какие кривые строятся для различных наборов опорных точек. Здесь вам поможет проект из подкаталога Ex42, где cpeди четырех опорных точек имеется одна выделенная. Клавишами управления курсором можно менять положение выделенной точки, при нажатии на Пробел выделенной становится следующая точка набора. Выделенная точка Рисуется красным Обратите внимание, что простой перерисовки окна при изменении в массиве опорных точек недостаточно, необходимо заново обратиться к командам, "заряжающим" вычислитель, чтобы пересчитать кривую:

If Key = VK_SPACE then begin
// выделенной становится следующая точка набора
selpoint: = selpoint + 1;
If selpoint > High (selpoint) then selpoint: = Low (selpoint);
InvalidateRect(Handle, nil, False);
end;
If Key = VK_LEFT then begin
// сдвигаем выделенную точку влево
ctrlpoints [selpoint, 0]: = ctrlpoints [selpoint, 0] - 0. 1;
// пересчитываем кривую по измененному массиву опорных точек
glMaplf(GL_MAPl_VERTEX_3, 0. 0, 1. 0, 3, 4, @ctrlpoints);
glEnable (GL_MAPl_VERTEX__3);
InvalidateRect(Handle, nil, False); // перерисовка окна
end;

С помощью этого примитивного редактора можно построить замысловатые фигуры и заодно получить представление о кривых Безье.

Замечание
Не получится расположить все четыре точки на кривой, если только это не линейная функция.

С помощью команды glGetMapfv в любой момент можно получить полную информацию о текущих параметрах вычислителя. Не думаю, что вы часто будете обращаться к этой команде, но на всякий случай приведу пример на ее использование (проект из подкаталога Ex43). Клавишами <Insert> и <Delete> можно менять текущее значение параметра вычислителя u2, в заголовке окна выводятся значения u1 и u2. Эти значения приложение получает от OpenGL:

wrk: Array [0.. 1] of GLfloat;
begin
glGetMapfv (GL_MAPl_VERTEX_3, Caption: = FloatToStr (wrk[0]
GL_DOMAIN, @wrk); + ', ' + FloatToStr(wrk[l]);

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

glMapGridlf (30, 0, 1);

Первый аргумент - количество подинтервалов, далее задается интервал по координате u. После того как сетка построена, вывод ее осуществляется одной командой:

glEvalMeshl (GL_LINE, 0, 30);

Первый аргумент - режим воспроизведения, отрезками или точками, остальные аргументы задают номера первой и последней точек рассчитанной сетки.
Получив представление о кривых, мы можем перейти к поверхностям Безье. Соответствующий пример располагается в подкаталоге Ex45, а результат работы программы показан на рис. 3. 29.

Рис. 3. 29. Классический пример на построение поверхности Безье

Массив опорных точек содержит координаты шестнадцати вершин. Работа программы начинается с установки параметров вычислителя:

glMap2f (GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, @ctrlpoints);
glEnable (GL_MAP2_VERTEX_3);
glMapGrid2f (20, 0. 0, 1. 0, 20, 0. 0, 1. 0);

У команды glMap2f аргументов больше, чем у glMaplf, но теперь нам легко понять их смысл. Первый аргумент - константа, определяющая тип рассчитываемых величин, в данном случае это координаты точек поверхности. Последний аргумент - указатель на массив контрольных точек поверхности.
Второй и третий параметры задают преобразования по координате u поверхности. Четвертый аргумент, как и в предыдущем случае, задает, сколько вещественных чисел содержится в порции данных - здесь мы сообщаем, что каждая точка задана тремя координатами. Пятый аргумент - количество точек в строке структуры, хранящей данные.
Следующие четыре аргумента имеют смысл, аналогичный предыдущим четырем, но задают параметры для второй координаты поверхности, координаты v. Значение восьмого аргумента стало равно двенадцати путем перемножения количества чисел, задающих координаты одной вершины (3), на количество точек в строке массива (4).
После задания параметров вычислителя он включается вызовом команды glEnable, после чего вызывается одна из пары команд, позволяющих построить поверхность - команда giMapGnd2f, рассчитывающая двумерную сетку. Первый и четвертый аргументы этой команды определяют количество разбиений по каждой из двух координат поверхности, остальные параметры имеют отношение к отображению интервалов.
Собственно изображение поверхности, двумерной сетки, осуществляется вызовом второй команды из тандема:

glEvalMesh2 (GL_FILL, 0, 20, О, 20);

Первый аргумент команды задает режим воспроизведения, следующие две пары чисел задают количество подинтервалов разбиения по каждой координате поверхности. В примере мы берем по 20 разбиений, столько же, сколько задано в команде giMapGnd2f, чтобы не выходить за пределы интервалов, но это не обязательно, можно брать и больше.
Если смысл параметров, связанных с отображением интервалов, вам кажется не совсем ясным, рекомендую вернуться к примеру по кривой Безье и еще раз его разобрать.
Замечу, что режим воспроизведения можно варьировать также с помощью Команды glPolygonMode.
Обратите внимание, что в рассматриваемом примере используется режим автоматического расчета нормалей к поверхности:

glEnable (GL_AUTO_NORMAL);

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

procedure TfrmGL.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
If Down then begin // кнопка мыши нажата
glRotatef (X - wrkX, 0.0, 1.0, 0.0);// поворот по горизонтали экрана
glRotatef (Y - wrkY, 1.0, 0.0, 0.0);// поворот по вертикали экрана
InvalidateRect(Handle, nil, False); // перерисовать экран
wrkX := X; // запоминаем координаты курсора
wrkY := Y;
end;
end;

Но при изменении размеров окна система координат возвращается в первоначальное положение.
Точно так же, как и в случае с кривой Безье, для воспроизведения поверхности можно воспользоваться командой glEvalCoord2f. Отрезки строятся по вершинам, предоставленным вычислителем:

9lBegm (GL_LINE_STRIP) ;
For i := 0 to 30 do For ] := 0 to 30 do
glEvalCoord2f (i / 30, j / 30);
glEnd;

Мы уже изучили множество параметров команды glEnable, задающей режимы воспроизведения и, в частности, позволяющей использовать вычислители. Я должен обязательно привести пример на команду, позволяющую определить, включен сейчас какой-либо режим или вычислитель - команду glIsEnabled. Ее аргумент - константа, символ определяемого режима, а результат работы, согласно документации, имеет тип GLboolean. Мы знаем о небольшой проблеме Delphi, связанной с этим типом, так что для вас не должно стать откровением то, что обрабатывать возвращаемое значение мы будем как величину булевского типа.
Приведу здесь простой пример, в котором до и после включения режима окрашивания поверхностей выводится сообщение о том, доступен ли этот режим:

If glIsEnabled (GL_COLOR_MATERIAL) = TRUE
then ShowMessage ('COLOR_MATERIAL is enabled')
else ShowMessage ('COLOR_MATERIAL is disabled');

Соответствующий проект располагается в подкаталоге Ex47.
В этом разделе мы рассмотрели, как в OpenGL строятся кривые и поверхности Безье, позже рассмотрим еще несколько примеров на эту тему.

NURBS-поверхности
Один из классов В-сплайнов, рациональные В-сплайны, задаваемые на неравномерной сетке (Non-Uniform Rational B-Spline, NURBS), является стандартным для компьютерной графики способом определения параметрических кривых и поверхностей.
Библиотека glu предоставляет набор команд, позволяющих использовать этот класс поверхностей. Будем знакомиться с соответствующими командами непосредственно на примерах и начнем с проекта из подкаталога Ex48, где строится NURBS-кривая по тем же опорным точкам, что и в первом примере на кривую Безье. Вид получающейся кривой тоже ничем не отличается от кривой, приведенной на рис. 3. 28.
Для работы с NURBS-поверхностями в библиотеке glu имеются переменные специального типа, используемые для идентификации объектов:

theNurb: gluNurbsObj;

При создании окна объект, как обычно, создается:

theNurb: = gluNewNurbsRenderer;

А в конце работы приложения память, занимаемая объектом, высвобождается:

gluDeleteNurbsRenderer (theNurb);

Замечание
В отличие от quadric-объектов, удаленные NURBS-объекты действительно более недоступны для использования.

Для манипулирования свойствами таких объектов предусмотрена специальная команда
библиотеки, в примере она используется для задания гладкости поверхности и режима воспроизведения. Гладкость кривой или поверхности задается допуском дискретизации: чем меньше это число, тем поверхность получается более гладкой:

gluNurbsProperty (theNurb, GLU_SAMPLING_TOLERANCE, 25. 0);

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

gluNurbsCurve (theNurb, 8, @curveKnots, 3, @ctrlpoints, 4, GL_MAPl_VERTEX_3 );

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

gluNurbsCurve (theNurb, 6, 8curveKnots, 3,
@ctrlpoints, 3, GL_MAPl_VERTEX_3);

Значения параметрических узлов в массиве или другой структуре, хранящей Данные, должны быть упорядочены по неубыванию, т. e. каждое значение не может быть меньше предыдущего. Обычно значения первой половины Узлов берутся равными нулю, значения второй половины задаются единичными, что соответствует неискаженной кривой, строящейся на всем интервале от первой до последней опорной точки. прежде чем мы сможем перейти к NURBS-поверхности, рекомендую самостоятельно поработать с этим примером, посмотреть на вид кривой при различных значениях параметров. Не забывайте о упорядоченности этих значений, и приготовьтесь к тому, что их набор не может быть совершенно произвольным: в некоторых ситуациях кривая строиться не будет.
Теперь мы можем перейти к поверхностям, и начнем с проекта из подкаталога Ex49 - модификации классического примера на эту тему, в котором строится холмообразная NURBS-поверхность (рис. 3. 30).

Рис. 3. 30. Классический пример на использование NURBS-поверхности

Первым делом замечу, что здесь появился новый для нас режим:

glEnable {GL_NORMALIZE);

Сделано это из-за того, что поверхность при построении масштабируется, и чтобы автоматически рассчитанные нормали "не уплыли", и используется этот режим.
Режим воспроизведения меняется по нажатию клавиши ввода, для его установки используется та же команда gluNurbsProperty:

If solid
then gluNurbsProperty(theNurb, GLU_DISPLAY_MODE, GLU_FILL)
else gluNurbsProperty(theNurb, GLU_DISPLAY_MODE, GLU_OUTLINE_POLYGON);

Команда собственно построения заключена в специальные командные скобки:

gluBeginSurface (theNurb); gluNurbsSurface (theNurb,
8, @knots,
8, @knots,
4 * 3,
3,
@ctrlpoints,
4, 4,
GL__MAP2_VERTEX_3);
gluEndSurface (theNurb);

В данном примере эти скобки не обязательны, поскольку они обрамляют единственную команду.
Если вы внимательно разобрали примеры предыдущего раздела, то большинство параметров команды gluNurbsSurface не должны вызывать вопросов, они аналогичны параметрам команд для построения поверхности Безье.
Так, шестой и седьмой параметры задают "большой шаг" по каждой координате, ассоциированной с поверхностью, т. e. сколько вещественных чисел содержится в строке структуры данных и сколько вещественных чисел задают отдельную точку. Восьмой параметр - адрес массива контрольных точек, а последним параметром задается символическая константа, определяющая тип возвращаемых значений; в данном случае ими являются трехмерные координаты вершин.
В примере задано шестнадцать контрольных точек, располагающихся равномерно по координатам X и Y в пределах квадрата, третья координата для точек, лежащих на границе квадрата, равна -3, для внутренних опорных точек эта координата равна 7. Таким способом массив заполняется для получения холмообразной поверхности. Если по заданным опорным точкам построить поверхность Безье, то увидим точно такой же холмик, как и в рассматриваемом примере.
Отличает NURBS-поверхности то, что параметризируемы. Так, предпоследние два параметра задают степень (порядок) поверхности по координатам u и v. Задаваемое число, как сказано в документации, должно быть на единицу больше требуемой степени. Для поверхности, кубической по этим координатам, число должно равняться 4, как в нашем примере. Порядок нельзя задавать совершенно произвольным, ниже мы разберем имеющиеся ограничения.
Второй параметр команды - количество узлов в параметризации по направлению, третьим параметром задается адрес массива, хранящего значения узлов. Третий и четвертый параметры команды имеют аналогичный смысл, но для второго направления.
Массивы узлов должны заполняться значениями, упорядоченными по неубыванию.
Как сказано в файле справки, при заданных uknot_count и vknot_count количествах узлов, uorder и vorder порядках количество опорных точек должно
ровняться (uknot_count - uorder) x (vknot_count - vorder). Так что при изменении порядка по координатам необходимо подбирать и все остальные параметры поверхности. если вы хотите подробнее узнать о параметризации и ее параметрах, то обратитесь к литературе с подробным изложением теории NURBS-поверхностей.
В данном примере используется один массив для задания узлов по обоим направлениям, а в проекте из подкаталога Ex50 используется два отдельных массива - для каждой координаты задаются свои значения узлов Поверхность строится не на всем интервале, а на части его, т. е. происходит подобие отсечения.
Чаще всего используются "кубические" NURBS-поверхности. Для иллюстрации построения поверхностей других порядков предназначен проект из подкаталога Ех51, где берется "квадратичная" поверхность
Библиотека glu предоставляет также набор команд для вырезки кусков NURBS-поверхностей. Примером служит проект из подкаталога Ех52 Опорные точки поверхности располагаются в пространстве случайно, а за)ем из поверхности вырезается звездочка - рис. 3.31.

Рис. 3.31 Команды библиотеки glu позволяют строить невыпуклые многоугольники

Для хранения точек на границе вырезаемой области - звездочки - введен массив:

trim: Array [Q..2Q, 0..1] of GLfloat;

Массив заполняется координатами точек, лежащих поочередно на двух вложенных окружностях:

procedure InitTrim;
var
i: Integer;
begin
For i := 0 to 20 do
If Odd(i) then begin // нечетные точки - на внешней окружности
trim [i, 0] := 0.5 " cos (i * Pi / 10) + 0.5;
trim [i, 1] := 0.5 * sin (i * Pi / 10) + 0.5;
end
else begin // четные точки - на внутренней окружности
trim (if 0] := 0.25 * cos (i * Pi / 10) + 0.5;
trim [i, 1] := 0.25 * sin (i * Pi / 10) + 0.5;
end;
end;

Внутри операторных скобок построения NURBS-поверхности вслед за командой gluNurbsSurface задаем область вырезки:

gluBeginTrim (theNurb) ;
gluPwlCurve (theNurb, 21, Qtnm, 2, GLU_MAP1_TRIM__21 ;
gluEndTrim (theNurb);

При задании области вырезки используются опять-таки специальные командные скобки, собственно область вырезки задается командой gluPwlCurve. Команда задает замкнутую кусочно-линейную кривую, часть NURBS-поверхности, не вошедшая внутрь этой области, не воспроизводится. Второй аргумент - количество точек границы, третий - адрес массива этих точек, четвертым параметром является символьная константа, задающая тип вырезки.
В примере при каждом нажатии пробела вызывается процедура инициализации поверхности, так что вид звездочки каждый раз получается разным,
случайным.
Следующий пример (подкаталог Ех53) также иллюстрирует вырезку NURBS-поверхности (рис. 3 32).

Рис. 3.32. Вырезка внутри NURBS-поверхности

Здесь вырезается внутренняя область поверхности, для чего задаются две линии, связанные с вырезкой:
gluBeginTrim (theNurb);

gluPwlCurve (theNurb, 5, SedgePt, 2, GLU_MAPl_TRIM_2);
gluEndTrim (theNurb);
gluBeginTrim (theNurb);
gluNurbsCurve (theNurb, 8, @curvcKnots, 2,
@curvcKnots, 4, GLU_MAPl_TRIM_2);
gluPwlCurve (theNurb, 3, PpwlPt, 2, GLL_MAP1_TRIM_2) ;
gluEndTrim (theNurb);

Первая линия охватывает весь интервал по обеим координатам поверхности, в массиве
  указаны точки границы по часовой стрелке:

edgePt : Array [0..4, 0..1] of GLfloat = ((0.0, 0.0), (1.0, 0.0),
(1.0, 1.0), (0.0, 1.0), (0.0, 0.0));

Теперь следующая область вырезки ограничивает внутреннюю вырезаемую область. В примере эта область состоит из двух кривых; если это усложняет понимание программы, строку с вызовом gluNurbsCurve можете удалить, а массив граничных точек дополните замыкающей точкой:

pwlPt : Array [0..3, 0..1] of GLfloat = ((0.75, 0.5), (0.5, 0.25),
(0.25, 0.5), (0.75, 0.5));

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

Дисплейные списки
В этом разделе мы познакомимся с одним из важнейших понятий OpenGL Дисплейные списки представляют собой последовательность команд, запоминаемых
для последующего вызова. Они подобны подпрограммам и являются удобным средством для кодирования больших систем со сложной иерархией.
Знакомство начнем с проекта из подкаталога Ех55, классического примера на эту тему Работа приложения особо не впечатляет: рисуется десять треугольников и отрезок (рис 3.33).

Рис. 3.33. Первый пример на использование дисплейных списков

В начале работы вызывается процедура инициализации, где создается (описывается) дисплейный список:

const
listName : GLUint = 1; // идентификатор списка
procedure init;
begin
glNewList (listName, GL_CCMPILE}; // начало описания списка
glColor3f (1.0, 0.0, 0.0);
glBegin (GL_TRIANGLES);
glVertex2f (0.0, 0.0);
glVertex2f (1.0, 0.0);
glVertex2f (0.0, 1.0);
glEnd;
glTranslatef (1.5, 0.0, 0.0);
glEndList; // конец описания списка
end;

Описание списка начинается с вызова команды glNewList, первым аргументом которой является целочисленный идентификатор - имя списка (в примере именем служит константа). Вторым аргументом команды является символическая константа; здесь мы указываем системе OpenGL на то, что список компилируется для возможного последующего использования. Все следующие до glEndList команды OpenGL и образуют дисплейный список, единую последовательность.
В данном примере в списке задается красный цвет примитивов, далее следуют команды для построения одного треугольника, заканчивается список Переносом системы координат по оси X.
В коде воспроизведения кадра цвет примитивов задается зеленым, затем десять раз вызывается описанный список.

glColor3f (0.0, 1.0, 0.0); // текущий цвет - зеленый
glPushMatrix; // запомнили систему координат
For i := 0 to 9 do // десять раз вызывается список с именем
glCallList (listName);
drawLine; // построить отрезок
glPopMatixx; // вернулись в запоганенную систему координат

Списки вызываются командой glcallList, единственным аргументом которой является идентификатор вызываемого списка.
В примере после каждого вызова списка номер один система координат смещается, поэтому на экране получается ряд треугольников, а отрезок рисуется вслед за последним треугольником. Хотя цвет примитивов задается зеленым, в списке он переустанавливается в красный, поэтому отрезок рисуется именно таким цветом.
Новички часто путаются, считая дисплейные списки полной аналогией подпрограмм. Отличает списки от подпрограмм то, что списки не имеют параметров, команды при вызове списка действуют точно так же, как и при создании списка. Если при описании списка используются переменные, то при вызове списка значения этих переменных никак не используются. Впрочем, ряд команд, помещенных в список, при вызове будут отрабатываться каждый раз заново, в документации приведен список таких команд.

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

По окончании работы память, занятую списками, необходимо освобождать, как это делается в этом примере:

glDeleteLists (listNamer 1);

Первый аргумент команды - имя первого удаляемого списка, второй параметр - количество удаляемых списков. Удаляются все списки, следующие за указанным. Стоит напомнить, что удалять списки необходимо до освобождения контекста воспроизведения.
В продолжение темы рассмотрим проект из подкаталога Ех56, Результат работы приложения приведен на рис. 3.34.

Рис 3.34. В этом примере цветовые настройки при использовании списка не сбиваются

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

gl=,-cName := glGenLists (1);

То есть вы можете и не затрачивать усилий на то, чтобы самому следить за именами используемых списков, а полностью положиться в этом вопросе на OpenGL. Это особенно удобно при интенсивной работе со списками, формируемыми в программе динамически.
Во-вторых, обратите внимание, что каждый вызов списка не портит текущие цветовые настройки. Корректность работы обеспечивается использованием пары новых для нас команд: glPushAttrib

glPopAttrib:
glNewList (listName, GL_COMPILE);
glPushAttrib (GL_CURRENT_BIT); // запомнили текущие атрибуты цвета
glColor3fv (@color_vector); // установили нужный цвет
glBegin (GL_TRIANGLES); // отдельный треугольник
glVertex2f (0.0, 0.0);
glVertex2f (1.0, 0.0);
glVertex2f (0.0, 1.0) ;
glEnd;
glTranslatef (1.57 0.0, 0.0);
glPopAttrib; // восстановили запомненные настройки
glSnaList;

Аналогично командам сохранения и восстановления системы координат, эта пара команд позволяет запоминать в отдельном стеке текущие настройки и возвращаться к ним. Аргумент команды glPushAttrib - символическая константа - задает, какие атрибуты будут запоминаться (значение в примере соответствует запоминанию цветовых настроек). Из документации можно Узнать, что существует целое множество возможных констант для этой команды; если вам покажется сложным разбираться со всеми ними, то пользуйтесь универсальной константой GLJ\LLJYTTRIB_BITS, в этом случае в стеке атрибутов запомнятся разом все текущие установки.
При перерисовке окна текущая система координат запоминается перед вызовом списков и восстанавливается перед прорисовкой отрезка:

glPushMatrix;
For i := 0 to 9 do
glCallList (listName);
glPopMatrix;
drawLine;

Поэтому в примере отрезок рисуется поверх ряда треугольников.
Следующий пример, проект из подкаталога Ех57, является очень простой иллюстрацией на использование команды glisList, позволяющей узнать, существует ли в данный момент список с заданным именем. Аргумент команды - имя искомого списка, возвращает команда величину типа GLboolean (как всегда в таких случаях, в Delphi обрабатываем результат как булевскую переменную):

If glisList (listName)
then ShowMessage ('Список с именем listName существует')
else ShowMessage ('Списка с именем listName не существует');

Теперь перейдем к проекту из подкаталога Ех58. Результат работы программы отличается от первого примера этого раздела только тем, что рисуется восемь треугольников вместо десяти, но суть примера не в этом: здесь иллюстрируется то, что при описании списка можно использовать вызов других списков. В примере описано три списка:

const // идентификаторы используемых списков
listNamel : GLUint = 1;
listName2 : GLUint = 2;
listName3 : GLUint = 3;
procedure unit;
var
i : GLuint;
begin
glNewList (listNamel, GL_COMPILE); // список 1 - отдельный треугольник
glColorSf (1.0, 0.0, 0.0);
glBegin (GL_TRIANGLES);
glVertex2f (0.0, 0.0);
glVertex2f (1,0, 0.0);
glVertex2f (0.0, 1.0);
glEnd;
glTranslatef (1.5, 0.0, 0.0);
glEndList;

glNewList (listName2, GL_COMPILE); // список 2 - четыре треугольника
For i := 0 to 3 do
glCallList (listNamel); // вызывается прежде описанный список 1
glEndList;
glNewList <listName3, GL_COMPILE); // список З - четыре треугольника
glCallList (listName21; // вызывается прежде описанный список 2
glEndList;
end;

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

const list : Array [0..1] of GLUint = (2, 3);

Этот массив используется при воспроизведении кадра в команде, позволяющей вызвать сразу несколько дисплейных списков:

glCallLists (2, GL_INT, @list);

Первый аргумент команды - количество вызываемых списков, второй аргумент указывает смещение, третий аргумент - указатель на структуру, содержащую имена вызываемых списков. Смещение задано согласно описанию используемого массива из указанного в файле справки списка возможных констант.
В примере вначале вызывается список с именем 2 и строятся первые четыре треугольника. Затем вызывается список с именем 3, который состоит во вторичном вызове списка номер 2.

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

Последний пример этого раздела, проект из подкаталога Ех59, по своему Функциональному действию ничем не отличается от предыдущего, однако Для нас в нем интересна новая команда - glListBase. Смысл ее очень прост - она задает базовое смещение для имен вызываемых списков.
В примере это смещение задано единичным:

3U,istBase (1);

А массив имен вызываемых списков содержит значения на единицу меньше, чем в предыдущем примере:

Ust : Array [0..1] of GLUint = (1, 2);

При вызове списков командой glCallLists к их именам
прибавляется заданное смещение.

Tess-обьекты
Мозаичные (tesselated - мозаичный) объекты являются последним нововведением библиотеки glu, предназначены они для упрощения построений невыпуклых многоугольников.
После того как мы изучили основные объекты библиотеки glu, нам будет несложно освоиться с собственно tess-объектами.
Работа с ними в DeIphi сопряжена с трудностями, возникающими из-за того, что раздел заголовочного файла, посвященный этим объектам, содержит несколько ошибок. Рис. 3. 39 демонстрирует работу примера проекта, расположенного в под каталоге Ex60.

Рис. 3. 35. Tess-объекты можно использовать для тех же целей, что и NURBS-поверхности

В программе определен особый тип для хранения координат одной точки:

type
TVector = Array [Q.. 2] of GLdouble;

Для уверенной работы команд данного раздела следует использовать именно гип удвоенной точности.
В процедуре инициализации описана переменная специального типа, введенного для работы с мозаичными объектами. Она инициализируется приблизительно так же, как другие объекты библиотеки glu, но, конечно, другой командой:

var

tobj: gluTesselator;
...
tobj: = gluNewTess;

Теперь посмотрим, как подготавливается список для левой фигуры, квадрата с треугольным отверстием внутри.
С помощью команды gluTessCallback задаются адреса процедур, вызываемых на различных этапах рисования tess-объекта, например:

gluTessCallback{totrj, GLU_TESS_BEGIN, @glBegin); //начало рисования

При начале рисования объекта мы не планируем особых манипуляций, поэтому просто передаем адрес процедуры qlBegin.
Если же нам по сценарию потребуется выполнить какие-то дополнительные действия, необходимо описать пользовательскую несвязанную (не являющуюся частью описания класса) процедуру, обязательно указав ключевое слово stdCall, и передавать адрес этой процедуры.
Синтаксис описания подобных процедур описан в справке по команде glutessCallback. Например, если мы хотим, чтобы перед воспроизведением примитива подавался бы звуковой сигнал, необходимо вызвать следующую процедуру:

procedure beginCalIback(which GLenum); stdcall;
begin
MessageBeep (MB_OK);
glBegin(which);
end;
...
gluTessCallback(tobj, GLU_TESS_BEGIN, @ BeginCallback);

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

gluTessCallback (tobj, GLU_TESS_VERTEX, @glVertexJdv); // вершина
glutessCallback(tobj, GLU_TESS_SND, 3glEnd); // конец рисования

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

procedure errorCallback(errorCode: GLenum); stdcall;
begin
ShowMessage (gluErrorString(errorCode));
end;
...
gluTessCallback(tobj, GLU_TESS_ERROR, @errorCallback]; //ошибка

Команда gluErrorstring возвращает строку с описанием возникшей ошибки. Описание ошибки выдается на русском языке (на языке локализации операционной системы), так что с диагностикой проблем не будет.
Координаты вершин квадрата и треугольника вырезки хранятся в структурах, единицей данных которых должна быть тройка вещественных чисел:

const
rect: Array [0.. 3] of TVector = ((50. 0, 50. 0, 0. 0),
(200. 0, 50. 0, 0. 0),
(200. 0, 200. 0, 0. 0),
(50. 0, 200. 0, 0. 0));
tri: Array[0.. 2] ofTVector= ((75. 0, 75. 0, 0. 0),
(125. 0, 175. 0, 0. 0),
(175. 0, 75. 0, 0. 0));

Наша фигура строится приблизительно по таким же принципам, что и в примере на вырезку в NURBS-поверхности:

glNewList(l, GL_COMPILE);
glColor3f(0. 0, 0. 0, 1-0); //цвет-синий
gluTessBeginPolygon (tobj, nil); // начался tess-многоугольник
gluTessBeginContour(tobj); // внешний контур - квадрат
gluTessVertex(tobj, @rect[0], @rect[Q]); // вершины квадрата
gluTessVertex(tobj, @rect[l], @rect[l]);
gluTessVertex(tob], @rect[2], @rect[2]);
gluTessVertex(tobj, @rect[3], @rect[3]);
gluTessEndContour (tobj);
glutessBeginContour(tobj); // следующие контуры задают вырезки
gluTessVertex(tobj, @tri[0], @tri[0]); // треугольник
gluTessVertex(tobj, @tn[l], @tri[l]);
gluTessVertex(tobj, @tn[2], @tri[2]);
gluTessEndContour(tobj);
gluTessEndPolygon(tobj); // закончили с tess-многоугольником
glEndList;

При перерисовке окна просто вызывается список с именем 1.
После того как список описан, tess-объект можно удалить, это делается в конце процедуры инициализации:

gluDeleteTess(tobj);

Замечание
Обратите внимание: при вызове списка сами объекты библиотеки glu уже не используются. Точно так же вы можете удалять quadric-объекгы сразу после описания всех списков, использующих их.

Надеюсь, с первой фигурой вам все понятно, и мы можем перейти ко второй фигуре, звездочке.
Здесь для наглядности перед вызовом каждой вершины текущий цвет меняется, для чего описана специальная процедура, адрес которой задается вызовом команды gluTessCallback:

procedure vertexCallback (vertex: Pointer); stdcall;
begin
glColor3f (random, random, random);
glVertex3dv (vertex);
end;
...
gluTessCallback{tobj, GLU_TESS_VERTEX, @vertexcallback);

Массив, хранящий координаты вершин звездочки, заполняется приблизительно так же, как в одном из предыдущих примеров на NURBS-поверхность. Многоугольник второго списка состоит из единственного контура. Перечисляем вершины, хранящиеся в массиве:

glNewList(2, GL_COMPILE);
gluTessBeginPolygon (tobj, nil);
gluTessBeginContour(tobj}; For i: = 0 to 20 do
gluTessVertex(tobj, @star [i], @star [i]);
gluTessEndContour (tobj);
gluTessEndPolygon (tobj);
glEndList;

Прототип одной из используемых команд мне пришлось переписать:

procedure gluTessBeginPolygon (tess: GLUtesselator; polygon_data:
Pointer); stdcall; external GLU32;

To, что записано в стандартном заголовочном файле, расходится с документацией и приводит к ошибке.
Мы рассмотрели простейший пример на использование tess-объектов, и надеюсь, вы смогли оценить, как удобно теперь становится рисовать невыпуклые многоугольники.
В качестве исследования можете задать контурный режим воспроизведения многоугольников, чтобы увидеть, как строятся получающиеся фигуры. Приведу еще несколько примеров на мозаичные объекты. Подкаталог Ex6l содержит проект, где строится объект в виде звездочки (рис. 3. 36)

Рис. 3. 36. Звездочка построена по координатам пяти вершин

На первый взгляд вам может показаться, что этот пример проще предыдущего, однако это не так: звездочка задана координатами пяти вершин, перечисляемых в том порядке, в котором обычно дети рисуют пятиконечные звезды. Мне пришлось изрядно потрудиться, чтобы этот пример заработал.
Если попытаться в предыдущем примере задать вершины звезды именно по такому принципу, мы получим сообщение об ошибке "требуется составной ответный вызов" (на русском). To есть серверу надо подсказать, как обрабатывать ситуации с пересечениями границ контура.
Почерпнув всю требуемую информацию из файла справки, я написал следующую процедуру и задал ее адрес для вызова обработчика пересечения:

gluTessCallback(tobj, GLU_TESS_COMBTNE, @combineCallback);

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

gluTessPropertyCall, GLU_TESS_WINDING R'JLE, GLU_TESS_WINDING POSlTIVE);

Обратите внимание, что значение первой символической константы в программе переопределено, в файле opengl. pas это значение задано неверно.
Предоставляю вам еще один пример на эту тему, проект из подкаталога Ex62. Не стану разбирать этот пример, чтобы не испугать начинающих Если он вам покажется трудным, можете отложить его подробное изучение на потом - до того момента, когда вы будете чувствовать себя с OpenGL совсем уверенно.
По ходу подготовки этого примера я обнаружил массу ошибок все в том же заголовочном файле, так что мне самому он дался очень тяжело.

Таймеры и потоки
В этом разделе мы познакомимся с различными способами создания анимации. При этом нам придется сосредоточиться на вопросах, больше связанных с операционной системой, чем с OpenGL.
Для самых простейших задач вполне подходит использование обычного таймера Delphi.
Посмотрим проект из подкаталога Ex63. Это немного модифицированный пример второй главы, в котором вокруг курсора рисовалось облачко отрезков. Теперь это облачко постоянно меняется так, что картинка еще более напоминает бенгальский огонь.
Первое изменение в проекте заключается в том, что на форме появился таймер, обработчик которого заключается в том, что десять раз в секунду окно перерисовывается. При каждой перерисовке окна вокруг курсора рисуется множество отрезков со случайными координатами конца.
Важно обратить внимание на действия, позволяющие ускорить работу приложения. Вместо традиционного для проектов Delphi вызова метода Refresn окно перерисовывается вызовом функции API (мы уже убедились, насколько это значимо):

procedure TfrmGL. TimerlTimer{Sender: TObject};
begin
lnvalidateRect(Handle, nil, False);
end;

Цвет окна формы я намеренно задал ярко-синим, чтобы проиллюстрировать, как важно в таких приложениях бороться за каждую миллисекунду. Если в обработчике таймера поставить Refresh, то при каждой перерисовке окно мерцает, по нему пробегают синие полосы Впрочем, может случиться и так, что на вашем компьютере такие эффекты не возникают, тут многое зависит от характеристик "железа".
Также для ускорения работы в этом примере вместо canvas. Handle используется явно полученная ссылка на контекст устройства.
Код перерисовки окна максимально сокращен, в нем оставлено только то, что не переносится в другие обработчики. Включение режима штриховки и задание области вывода перемещены в обработчики onCreate и onResize Формы, соответственно. В таких приложениях также желательно использовать перехватчик сообщения WM_PAINT вместо обработчика onPaint. Это сделано в следующем примере, проекте из подкаталога Ex64, в котором экране двигаются по кругу шесть кубиков (рис. 3. 37).

Рис. 3. 37. При работе программы кубики вращаются по кругу

Положения центров кубиков хранятся во вспомогательных массивах, заполняемых при начале работы приложения синусами и косинусами:

For i: = 0 to 5 do begin
wrkX [i] : = sin (Pi / 3 * i);
wrkY [i] : = соs (Pi / 3 * j);
end;

Поворот всей системы с течением времени обеспечивается тем, что в обработчике таймера значение переменной, связанной с углом поворота, увеличивается, после чего экран перерисовывается:

Angle: = Angle + 1; // значение угла изменяется каждый "тик"
If Angle >= 60. 0 then Angle: = 0. 0;
InvalidateRect(Handle, nil, False);

Ядро кода воспроизведения кадра выглядит так:

glPushMatrix; // запомнили начальную систему координат
glRotatef(Angle, 0. 0, 0. 0, 1. 0); // поворот системы на угол
Angle по 2 {Цикл рисования шести кубиков}
For i: = С to 5 do begin glPushMatrix; // запомнили систему координат
glTranslatef(wrkX [i], wrkY [i], 0. 0); // перенос системы координат
glRotatef(-60 * i, 0. 0, 0. 0, 1. 0); // поворот кубика
glutSolidCube (0. 5);
glPopMatrix; end;
// рисуем кубик
// вернулись в точку
glPopMatrix; // вернулись в первоначальную систему координат

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

Замечание
Приведет это к тому, что кубики будут вращаться все быстрее и быстрее: теперь они при каждом тике таймера поворачиваются на все более увеличивающийся угол Angle относительно предыдущего положения Можно использовать более оптимальный прием в подобных примерах не использовать управляющую переменную (здесь это Angle}, не использовать команды glPushmatrix и glPopMatrix, а код кадра начинать с поворота на угол, константу. С точки зрения скорости это оптимально, но может нарушать сценарий кадра' ведь при изменении размеров окна мы принудительно возвращаем объекты сцены в первоначальную систему координат, и кубики резко дергаются.

Использование системного таймера является самым простым решением задачи, но имеет очевидные недостатки. На маломощных компьютерах уже этот пример выводит кадры рывками, а если количество объектов перевалит за два десятка, то удовлетворительную скорость воспроизведения можно будет получить только на очень хороших машинах.
Следующий пример, проект из подкаталога Ex65, является продолжением предыдущего, здесь рисуется пятьдесят параллелепипедов (рис. 3. 38).

Рис. 3. 38. Эту систему мы возьмем в качестве тестовой для сравнения методов создания анимации

Вся система вращается по двум осям, по оси Y вращение происходит с удвоенной скоростью:

glRotatef(2 * Angle, 0. 0, 1. 0, O. 0}; // поворот по ось Y
glRotatef(Angle, 0. 0, 0. 0, 1. 0}; // поворот по оси Z

Интервал таймера я задал равным 50 миллисекунд, т. e. экран должен обновляться двадцать раз в секунду. Попробуем выяснить, сколько кадров В секунду выводится в действительности.
Это делается в проекте из подкаталога Ex66. Введены переменные, счетчики кадров и количество кадров в секунду:

newCount, frameCount, lastCount: LongInt;
fpsRate: GLfloat;

При запуске приложения инициализируем значения:

lastCount: = GetTickCount;
frameCount: = 0; ;

Функция API GetTickCount возвращает количество миллисекунд, прошедших с начала сеанса работы операционной системы
При воспроизведении кадра определяем, прошла ли очередная секунда, и вычисляем количество кадров, выведенных за эту секунду:

newCount: = GetTickCount; // текущее условное время
Inc(frameCount); // увеличиваем счетчик кадров
If (newCount - lastCount) > 1000 then begin // прошла секунда
// определяем количество выведенных кадров
fpsRate: = frameCount * 1000 / (newCount - lastCounU;
// выводим в заголовке количество кадров
Caption: = 'FPS - ' + FloatToStr {fpsRate);
lastCount: = newCount; // запоминаем текущее время
frameCount: - 0; // обнуляем счетчик кадров
end;

Получающееся количество воспроизведенных кадров в секунду зависит от многих факторов, в первую очередь, конечно, от характеристик компьютера, и я не могу предсказать, какую цифру получите именно вы. Будет совсем неплохо, если эта цифра будет в районе двадцати.
Но если задаться целью увеличить скорость работы этого примера, то выяснится, что сделать это будет невозможно, независимо от характеристик компьютера. Сколь бы малым не задавать интервал таймера, выдаваемая частота воспроизведения не изменится, системный таймер в принципе не способен обрабатывать тики с интервалом менее пятидесяти миллисекунд Еще один недостаток такого подхода состоит в том, что если обработчик тика таймера не успевает отработать все действия за положенный интервал времени, то последующие вызовы этого обработчика становятся в очередь. Это приводит к тому, что приложение работает с разной скоростью на различных компьютерах.
Мы не будем опускать руки, а поищем другие способы анимации, благо их существует несколько. Рассмотрим еще один из этих способов (я его нахожу привлекательным), состоящий в использовании модуля MMSystem (Multimedia System). Мультимедийный таймер позволяет обрабатывать события с любой частотой, настолько часто, насколько это позволяют сделать ресурсы компьютера.
Посмотрим на соответствующий пример - проект из подкаталога Ex67 Здесь рисуются все те же пятьдесят параллелепипедов, но частота смены кадров существенно выше.
Список подключаемых модулей в секции implementation дополнился модулем MMSystem:

uses DGLUT, MMSystem;

Мультимедийный таймер также нуждается в идентификаторе, как и обычный системный, описываем его в разделе private класса формы:

TimerId: uint;

А вот процедура, обрабатывающая тик таймера, не может являться частью класса:

procedure TimeProc (uTimerID, uMessage: UINT; dwUser, dwl, dw2: DWORD; stdcall;
// значение угла изменяется каждый "тик"
With frmGL do begin
Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: - 0. 0;
InvalidateRect (HandJe, nil, False);
end;
end;

При создании окна таймер запускается специальной функцией API:

TimerID: =timeSetEvent (2, 0, @TimeProc, 0, TIME_PERTGDIC);

По окончании работы приложения таймер необходимо остановить; если это не сделать, то работа операционной системы будет заметно замедляться;

timeKillEvent (TimerID);

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

Замечание
В документации рекомендуется задавать ненулевое значение для уменьшения системных потерь

Следующий параметр - адрес функции, ответственной за обработку каждого тика. Четвертый параметр редко используется, им являются задаваемые пользователем данные возврата Последним параметром является символическая константа, при этом значение TIME_PERIODIC соответствует обычному поведению таймера.
Итак, в примере каждые две миллисекунды наращивается угол поворота системы и перерисовывается экран.
Конечно, за две миллисекунды экран не будет перерисован, однако те кадры, которые компьютер не успевает воспроизвести, не будут накапливаться. При использовании мультимедийного таймера сравнительно легко планировать поведение приложения во времени: на любом компьютере и в любой ситуации система будет вращаться с приблизительно одинаковой скоростью, просто на маломощных компьютерах повороты будут рывками В продолжение темы посмотрите проект из подкаталога Ex68, где все тот же мультфильм рисуется на поверхности рабочего стола, подобно экранным заставкам. Во второй главе мы уже рассматривали похожий пример, здесь добавлена анимация, а команда glviewPort удалена, чтобы не ограничивать область вывода размерами окна приложения.

Замечание
Напоминаю, что такой подход срабатывает не на каждой карте

Наиболее распространенным способом построения анимационных приложений является использование фоновой обработки, альтернативы таймерам. Разберем, как это делается.
В Delphi событие onidle объекта Application соответствует режиму ожидания приложением сообщений. Все, что мы поместим в обработчике этого события, будет выполняться приложением беспрерывно, пока оно находится в режиме ожидания.
Переходим к примеру, проекту из подкаталога Ex69. Сценарий приложения не меняем, чтобы можно было сравнить различные способы. Отличает пример то, что в нем отсутствуют какие-либо таймеры; код, связанный с анимацией, перешел в пользовательскую процедуру:

procedure TfrmGL. Idle (Sender: TObject; var Done: boolean);
begin
With frmGL do begin
Angle: = Angle + 0. 1;
If Angle >= З60. 0 then Angle: = 0. 0;
Done: = False; // обработка завершена
InvalidateRect{Handle, nil. False);
end;
end;

Второй параметр Done используется для того, чтобы сообщить системе, требуется ли дальнейшая обработка в состоянии простоя, или алгоритм завершен. Обычно дается False, чтобы не вызывать функцию WaitMessage.
При создании окна устанавливаем обработчик события onidle объекта Application:

Application. OnIdle: = Idle;

Вот и все необходимые действия, можете запустить приложение и сравнить этот метод с предыдущим.

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

При использовании последнего способа у нас нет никакого контроля над выполнением кода, есть только весьма приблизительное представление о том. сколько времени будет затрачено на его выполнение. Скорость работы приложения в этом случае полностью зависит от загруженности системы, другие приложения могут с легкостью отнимать ресурсы, и тогда работа нашего приложения замедлится, Запустите одновременно приложения, coответствующие мультимедийному таймеру и фоновой обработке Активное приложение будет генерировать большую частоту кадров, но приложение, построенное на таймере, менее болезненно реагирует на потерю фокуса,
В таких случаях можно повышать приоритет процесса, этот прием мы рассмотрим в главе 5.
Замечу, что при запуске приложения или изменении ею размеров требуется несколько секунд, чтобы работа вошла в нормальный режим, поэтому первые цифры, выдаваемые в качестве частоты воспроизведения, не являются особо надежными.
Теперь посмотрим, как реализовать фоновый режим в проектах, основанных только на функциях API. Это иллюстрирует проект из подкаталога Ex70.
Пользовательская функция idle содержит код, связанный с изменениями кадра.
Для отслеживания состояния активности окна заведен флаг AppActive, а оконная функция дополнилась обработчиком сообщения, связанного с активацией окна:

WM__ACTIVATEAPP:
If (wParam = WMACTIVE) or (wParam = WM__CLICKACTIVE)
then AppActive: = True
else AppActive: = False;

Кардинальные изменения коснулись цикла обработки сообщений, вместо которого появился вот такой вечный цикл:

While True do begin
// проверяем очередь на наличие сообщения
If PeekMessage (Message, 0, 0, 0, pm_NoRemove) then begin
// в очереди присутствует какое-то сообщение
If not GetMessage(Message, 0, 0, 0)
then Break // сообщение WM_QUIT, прервать вечный цикл
else begin // обрабатываем сообщение
TranslateMessage(Message);
DispatchMessage(Message);
end;
end
else // очередь сообщений пуста
If AppActive
then Idle // приложение активно, рисуем очередной кадр
else WaitMessage; // приложение не активно, ничего не делаем
end;

Надеюсь, все понятно по комментариям, приведу только небольшие пояснения.
Функция PeekMessage с такими параметрами, как в этом примере, не удаляет сообщение из очереди, чтобы обработать его в дальнейшем традиционным способом.
Функция idle в этом примере вызывается при пустой очереди только в случае активности приложения.
Код можно немного сократить, если не акцентироваться на том, активно ли приложение; в данном же случае минимизированное приложение "засыпает". не внося изменений в кадр.
Рассмотрим еще один прием, использующийся для построения анимационных приложений и заключающийся в зацикливании программы. Для начала приведу самый простой способ зацикливания программы (проект из подкаталога Ex71).
Сценарий не изменился, рисуется все то же подобие шестерни. Никаких таймеров и прочих приемов, просто обработчик перерисовки окна заканчивается приращением управляющей переменной и командой перерисовки региона (окна):

Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: = 0. 0;
InvalidateRect(Handle, nil, False);

Все просто: воспроизведя очередной кадр, подаем команду на воспроизведение следующего.
В этом методе, как, впрочем, и в предыдущем, при уменьшении размеров окна частота кадров увеличивается, но и вращение происходит быстрее, здесь так же отсутствует контроль за поведением системы.
Приложение "замирает", будучи минимизированным
Если присмотреться внимательнее к поведению этого примера, то можно заметить некоторые необычные вещи, например, при наведении курсора на системные кнопки в заголовке окна подсказки не появляются. А при попытке активизации системного меню окна появляется явная накладка в работе приложения, область меню требуется перерисовать, двигая курсор в пределах его границ. Короче, зацикливание программы приводит к тому. что ожидающие сообщения могут и не обрабатываться. Может, это и не страшно, но ведь у нас нет гарантии, что мы обнаружили все странности работы приложения.
Решение проблемы состоит в использовании функции processMessages объекта Application, приостанавливающей работу приложения, чтобы система могла обрабатывать сообщения из очереди
Посмотрим на проект из подкаталога Ex72. Перед перерисовкой окна обрабатываем все сообщения очереди, вызвав функцию ProcessMessages, однако этого добавления не достаточно, иначе приложение невозможно будет закрыть. В примере введен вспомогательный флаг closed, принимающий истинное значение при попытке пользователя или системы закрыть приложение:

procedure TfrmGL. FormCloseQuery(3ender: TObject; var Car. Close: Boolean;
begin
Closed: = True end;

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

If not Closed then begin
Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: = 0, 0;
Application. ProcessMessages;
InvalidateRect{Handle, nil, False);
end;

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

Canvas. TextOut (0, 0, 'FPS - ' + FloatToStr (fpsRate));

При выводе на поверхность окна с помощью функций GDI не должно возникать проблем ни с одной картой, а вот если попытаться текст выводить на метку, то проблемы, скорее всего, возникнут со всеми картами: метка не будет видна.
Следующий пример, проект из подкаталога Ex73, является очередным моим переводом на Delphi классической программы, изначально написанной на С профессиональными программистами корпорации Silicon Graphics. Экран заполнен движущимися точками так, что у наблюдателя может появиться ощущение полета в космосе среди звезд (рис. 3. 39).

Рис. 3. 39. Проект Stars создает иллюзию полета в космосе

Предусмотрены два режима работы программы, управление которыми осуществляется нажатием пробела и клавиши T'. После нажатия пробела некоторые звезды летят по "неправильной" траектории, после нажатия второй управляющей, клавиши происходит "ускорение" полета на некоторое время.
Последний прием, который мы разберем в этом разделе, основан на использовании потоков. Проект из подкаталога Ex74 иллюстрирует этот прием на примере хорошо нам знакомой по предыдущим упражнениям вращающейся шестерни. В программе введен тип, ответственный за используемый поток:

type
TGLThread = class (TThread)
protected
procedure Execute;
override; // метод обязательно переопределяется
procedure Paint; // пользовательский метод потока
end;

Два метода потока описаны в традиционном стиле:

procedure TGLThread. Paint;
begin
With frmGL do begin
Angle: = Angle + 0. 1;
If Angle >=o 360. 0 then Angle: = 0, 0;
InvalidateRect(Handle, nil. False);
end;
end;
procedure TGLThread. Execute;
begin repeat
Synchronize (Paint); // синхронизация потоков
until Terminated;
end;

После создания окна поток инициализируется и запускается:

GLThread: = TGLThread, Create (False);

По окончании работы приложения выполняются стандартные действия:

GLThread. 3uspend; // приостановить поток
3LThread. Free; // удалить поток

В этом разделе мы рассмотрели несколько способов организации анимационных программ. У каждого из этих методов есть свои достоинства и свои недостатки, и вы вольны самостоятельно решать, какой из этих способов является для вас наиболее подходящим. Если при уменьшении размеров окна частота воспроизведения увеличивается, это является положительной стороной метода, но если в данном методе невозможно предугадать поведение программы на разных машинах, то это можно отнести к его недостаткам. Повторю, что обработка ожидания является самым распространенным способом, и при обычной нагрузке системы он должен показать максимальную частоту воспроизведения.
В оставшихся примерах книги вы можете встретить самые разные из этих способов, я не стану придерживаться какого-либо одного. Приведу еще один пример на анимацию, проект из подкаталога Ex75, где используется обычный системный таймер. В примере рисуется фонтан из двyx тысяч точек (рис. 3. 40).

Рис. 3. 44. Проект Fontain, две тысячи капель

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

UpdatePOINT(i: Word); // процедура перемещения капли
begin
Points[i] [Qj: = points[i] [0] + motion[i] [0]; // изменение координат
points[i][l]. =points[i][l] +motion[i][l];
points[i][2]: =points[i][2] +motion[i][2];
If points[i][1] < -0. 75 then begin // капля фонтана упала на землю
points[i][0] = 0. 0; // новая капля вырывается из фонтана
points[i][1] = -0. 5;
points[i] [2] = 0. 0;
motion[i][0] = (Random-0. 5) / 20;
motion[i][l] = Random / 7 + 0. 01;
motion[i][2] = (Random-0. 5) / 20;
end
else motion[i][l}: = motion[i][1] - 0. 01; // условная сила тяготения
end;

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

Рис. 3. 45. Теперь вы умеете рисовать даже такие "художественные произведения"

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

var
Angle: GLint = 0;

Вы пользуетесь не таймером, а скажем, зацикливаете программу Шаг увеличения переменной вы задаете так, что на вашем компьютере движение объектов происходит ровно и с удовлетворительной скоростью

Angle: = (Angle + 2) mod 360;

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

angle: GLfloat = 0; time: LongInt;

Если по сценарию полный оборот должен произойти за десять секунд, то код должен быть таким:

Angle: = Angle + 0. 1 * (GetTickCount - time)*3600/1000
If Angle >= 360. 0 then Angle: = 0. 0;
time: = GetTickCount;


OpenGL в Delphi - ГЛАВА 4 Визуальные эффекты

ГЛАВА 4 Визуальные эффекты

Подробнее об источнике света
Свойства материала
Вывод на палитру в 256 цветов
Подробнее о поверхностях произвольной формы
Использование патчей
Буфер трафарета
Смешение цветов и прозрачность
Подробнее о пиксельных операциях
Буфер накопления
Туман
Тень и отражение
Шаблон многоугольников
Текстура

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

Подробнее об источнике света
Начнем с позиции источника света в пространстве. Раньше мы пользовались источником света со всеми характеристиками, задаваемыми по умолчанию Источник света по умолчанию располагается в пространстве в точке с координатами (0, 0, 1)
Поучимся менять позицию на примере проекта из подкаталога Ex01. На экране присутствует тор, вокруг которого вращается источник света Позиция источника света визуализируется каркасным кубиком. Игра теней на поверхности тора меняется в зависимости от текущего положения источника света.

Замечание
Напомню если режим GL_COLOR_MATERIAL не включен, то текущие цветовые установки не влияют на цвет поверхности тора, поэтому он выглядит серым хотя текущий цвет задан зеленоватым.

Переменная spin задает угол поворота системы координат, связанной с источником света по оси X Положение источника света в этой системе координат определено в массиве position

const
position : Array [0..3] of GLfloat = (0, 0, 0, 0, 1 5, 1.0);

Перерисовка кадра выглядит так:

glPushMatrix; // запомнили мировую систему координат
glRotated (spin, 1.0, 0.0, 0.0); // поворот системы координат
glLightfv (GL_LIGHTO, GL_POSITION, Sposition), // задаем новую позицию
// источника света glTranslated (0.0, 0.0, 1.5); // перемещаемся в точку, где
// располагается источник света
glDisable (GL_LIGHTING); // отключаем источник света
glutWireCube (0.1); // визуализируем источник света
glEnable (GL_LIGHTING); // включаем источник света
glPopMatrix; // возвращаемся в мировую систему координат
glutSolidTorus (0.275, 0.85, 8, 15), // рисуем тор

Требуются некоторые пояснения Кубик рисуется с отключенным источником света для того, чтобы он не получился таким же, как тор, серым. Большинство параметров источника света задается с помощью команды glLightfv Первый параметр команды - идентификатор источника, второй аргумент - символическая константа, указывающая, какой атрибут устанавливается, последним аргументом задается ссылка на структуру, содержащую задаваемые значения
Как видно из кода, для задания позиции символическая константа должна быть GL_POSITION, указываемый массив определяет позицию источника света в текущей системе координат Источник света, подобно невидимому объекту, располагается в заданной точке и не перемещается вслед за системой координат. После того как его позиция зафиксирована, трансформации системы координат не приведут к изменению положения источника.
В этом примере значения элементов массива не изменяются, а система координат перемещается с течением времени Можно и наоборот - изменять значения элементов массива, а систему координат не трогать. Это сделано в следующем примере, проекте из подкаталога Ех02, где положение источника света задается значениями элемента массива LightPos, изменяющимися с течением времени:

With frmGL do begin
LightPos[0] := LightPos[0] + Delta;
If LightPos[0] > 15.0
then Delta := -1.0
else If (LightPos[0] < -15 0) then
Delta := 1.0,
InvalidateRect(Handle, nil, False);
end,

Перерисовка кадра начинается с того, что задается текущее положение источника света:

glLightfv(GL__LIGHTO, OPPOSITION, @LightPos}; glCallList(Sphere);

В примере источник света колеблется над поверхностью сферы. Теперь настала пора разобрать параметры источника света, связанные с оптическими характеристиками окружающей среды Эти характеристики складываются из трех отражений: фоновое, диффузное и зеркальное Символьные константы GL_AMBIENT, GL_DIFFUSE и GL_SPECULAR, указанные в качестве второго аргумента команды glLight, позволяют задавать требуемые свойства окружающей среды.
Наиболее важными для нас пока являются первые две характеристики Я приведу их упрощенное толкование, которого вам будет вполне достаточно для успешного использования соответствующих команд. Конечно, эти понятия имеют физическое обоснование, и неплохо было бы разобраться в нем, но пока можно ограничиться и поверхностным представлением Вес цвета в диффузной составляющей задает, насколько сильно этот цвет отражается поверхностью при ее освещении. И наоборот, вес цвета в фоновой составляющей задает, насколько сильно этот цвет поглощается поверхностью.
По умолчанию поверхность ничего не поглощает и все отражает. Следующий пример, проект из подкаталога Ех03, является развитием предыдущего, в нем добавлена возможность задания значений для характеристик источника цвета.
Окно приложения снабжено всплывающим меню. По выбору пункта меню появляется диалог задания цвета; выбранный пользователем цвет устанавливается значением нужной характеристики источника света. Здесь я воспользовался разобранной во второй главе процедурой, которая переводит системный цвет в диапазон, обычный для OpenGL:

procedure TfrmGL.AmbientlClick(Sender: TObject);
begin
If ColorDialogl.Execute then ColorToGL (ColorDialogl.Color, Ambient [0], Ambient [I], Ambient 12);
end;

С помощью этого проекта можно подбирать свойства источника света для дальнейшего использования в других проектах. При выборе пункта Info появляется окно, в котором выводятся текущие характеристики источника света (рис. 4.1).

Рис. 4.1. В примере можно менять текущие установки источника света

Пункт меню Reset позволяет вернуться к первоначальным установкам, соответствующим принятым в OpenGL по умолчанию
Проведем простые эксперименты. Задайте фоновое отражение каким-либо чистым цветом, например красным. После этого наименее освещенные части поверхности сферы окрашиваются этим цветом. Если значения цветового фильтра, RGB, тоже установить красными, вся сфера окрасится равномерно.
Верните значения установок в первоначальные и установите значения диффузного отражения в тот же чистый цвет. Теперь в этот цвет окрашиваются участки поверхности сферы, наиболее сильно освещенные источником света. Слабо освещенные участки окрашены в оттенки серого. Если RGB установить в этот же цвет, насыщенность им увеличивается по всей поверхности. Если обе характеристики задать равными, поверхность равномерно окрашивается ровным светлым оттенком.
Проект этот простой, но весьма интересный. Самые красивые результаты получаются при цветовом фильтре из смеси базовых цветов и отражениях, заданных в чистые цвета.

Свойства материала
Помимо свойств материала в этом разделе мы также продолжим изучение свойств источника света.
Свойства материала задаются с помощью команды glMaterial Характеристики, определяющие оптические свойства материала, и соответствующие им символьные константы являются следующими" рассеянный цвет (GL_AMBIENT), диффузный цвет (GL_DIFFUSE), зеркальный цвет (GL_SPECULAR), Излучаемый цвет (GL_EMISSION), степень зеркального отражения (GL_ SHININESS).
Значением последнего параметра может быть число из интервала [0,128], oстальные параметры представляют собой массивы четырех вещественных чисел.
Зеркальный цвет задает цветовую гамму бликов материала, степень зеркального отражения определяет, насколько близка поверхность к идеальному зеркалу.
Поработайте с проектом из подкаталога Ех04 и выясните смысл всех характеристик материала "своими глазами".
Этот пример является продолжением предыдущего, в нем добавились пункты меню, соответствующие характеристикам материала. Важно заметить: чтобы цветовой фильтр не перебивал задаваемые свойства, в примере не включается режим GL_COLOR_MATERIAL.
Начните знакомство с примером с вариации значения степени зеркального отражения, которое определяет цветовую насыщенность блика от источника света. С помощью клавиши 'S' можно регулировать размер блика на сфере, нажимая ее одновременно с <Shift> - увеличить, а без <Shift> - уменьшить эти размеры.

Замечание
Чтобы блик появился на сфере, установите зеркальный цвет в значение, отличное от принятого по умолчанию.

Следующий пример, проект из подкаталога Ех05, знакомит нас с еще одним аспектом, связанным с источником света - с моделью освещения.
Модель освещения задается с помощью команды glLightModel. В примере внутренняя и внешняя стороны цилиндра имеют разные свойства материала, внутренняя сторона окрашена синим, внешняя - красным. Свойства материала заданы в массивах ambFront и ambBack. Для того чтобы включить освещенность для внутренней стороны многоугольников, вызывается команда glLightModel, у которой вторым аргументом задается символическая константа GL_LIGHT_MODEL_TWO_SIDE, а третьим аргументом - ноль или единица:

glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1); // для обеих сторон
glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, @ambFront); // задняя
// сторона
glMaterialfv (GL_BACK, GL_AMBIENT_AND_DIFFUSE, @ambBack); // передняя
// сторона

Я советую вам не спеша разобраться с этим упражнением. Например, посмотрите результат, который получается при замене константы GL_AMBIENT_ANC__DIFFUSE На GL_DIFFUSE И GL_AMBIENT. Вы увидите, что диффузный цвет материала
наиболее сильно определяет цветовые характеристики поверхности.
Следующий пример, проект из подкаталога Ех06, совсем простой - на экране вращается объемная деталь, построенная на основе тестовой фигуры второй главы (рис. 4.2).

Рис. 4.2. Теперь тестовая деталь выглядит более реалистично

В этом примере интересно то, как при некоторых положениях детали ее грани отсвечивают слабым фиолетовым оттенком.
Массив свойств материала детали содержит небольшую составляющую красного, чем больше это число (до некоторых пределов), тем выразительнее будет выглядеть деталь:

MaterialColor: Array [0..3] of GLfloat = (0.1, 0.0, 1.0, 1.0);

Как правило, по ходу работы приложения требуется менять текущие свойства материала, как это делается в проекте из подкаталога Ех07. На экране вращается кубик, "проткнутый" тремя цилиндрами (рис. 4.3).

Рис. 4.3. Объекты сцены имеют различные свойства материала

В программе заданы два массива, определяющие различные цветовые гаммы. Перед воспроизведением элемента задаются свойства материала:

const
MaterialCyan : Array[0..3] of GLfloat = (0.0, 1.0, 1.0, 1.0);
MaterialYellow : Array[0..3] of GLfloat = (1.0, 1.0, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, @MaterialCyan);
glutSolidCube (2.0); // зеленоватый кубик
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, @MaterialYellow);
// три желтых цилиндра
glTranslatef (0.0, 0.0, -2.0);
gluCylinder (qObj, 0.2, 0.2, 4.0, 10, 10);
glRotatef (90, 1.0, 0.0, 0.0);
glTranslatef (0.0, 2.0, -2.0);
gluCylinder (qObj, 0.2, 0.2, 4.0, 10, 10);
glTranslatef (-2.0, 0.0, 2.0);
glRotatef (90, 0.0, 1.0, 0.0);
gluCylinder (qObj, 0.2, 0.2, 4.0, 10, 10);

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

Рис. 4.4. Для прямоугольника, расположенного слева, источник света находится в бесконечности

Позиция источника света, освещающего первый четырехугольник, задана точно такой же, что и для второго квадрата, но значение четвертого компонента равно нулю, источник света расположен где-то в бесконечности, и только направление на него влияет на оттенок серого, которым равномерно окрашивается плоскость
Далее в нашей программе стоит проект из подкаталога Ех09. Этот пример, представляющий двенадцать сфер из разного материала, обычно сопровождает любой курс по OpenGL (рис. 4 5)

Рис. 4.5. Классический пример, иллюстрирующий свойства материала

Стоит сразу же обратить внимание, что при отключенном режиме GL_COLOR_ MATERIAL цветовой фильтр никак не влияет на освещенность. Для регулирования ее гаммы используется команда задания модели освещения со вторым аргументом, равным GL_LIGHT_MODEL_AMBIENT, при этом последний аргумент, массив четырех вещественных чисел, задает цветовую палитру:

glLightModelfv(GL_LIGHT_MODEL_AMBIENT, @lmodel_ambient);

Сфера в первом ряду и первой колонке нарисована с отключенными фоновой и зеркальной составляющими материала Первый ряд, вторая колонка - диффузное и зеркальное освещение, следующая сфера более блестящая. Последняя сфера первого ряда - включены диффузия и эмиссия. Фоновая и диффузная составляющие включены для первой сферы второй линии, нет зеркальной составляющей Вторая и третья сферы - все включено, кроме эмиссии, различаются размером блика. У последней сферы второго ряда отсутствует зеркальная составляющая.
Сферы последней линии отличаются усиленностью цветовой насыщенности вставляющих свойств материала.
Еще один классический пример, проект из подкаталога Ех10 двадцать чайников из различного материала (рис. 4.6).

Рис. 4. 6. Новичков такие картинки обычно впечатляют

Не будем рассматривать программу подробно, мы не встретим в ней ничего нового, однако пример этот не бесполезный - подобранный профессионалами набор материалов может вам пригодиться. В примерах пятой и шестой глав я использовал некоторые из этих материалов. Поскольку чайники появляются на экране сравнительно медленно, в этом примере не используется двойная буферизация. Дальше мы рассмотрим серию моих переложений на Delphi программ из набора примеров OpenGL SDK и закрепим наши знания по теме этого раздела. Начнем с проекта из подкаталога Ex11, на котором основаны и несколько следующих примеров. На экране располагаются конус, тор и сфера (рис. 4.7).

Рис. 4. 7. Эту композицию будем использовать в качестве тестовой

Все параметры, за исключением позиции источника света, задаются по умолчанию. Последнее число (w-компонент) в массиве, связанном с позицией источника света, равно нулю, источник света располагается на бесконечности.
Здесь все просто, можем перейти к следующему примеру, проекту из подкаталога Ex12. Объекты сцены окрашены в фиолетовый цвет, добавилась строка, задающая диффузную составляющую источника света:

glLightfv(GL_LIGHTO, GL_DIFFUSE, @light_diffuse);

В проекте из подкаталога Ex13 - другая картина, возникшая благодаря тому, что задается фоновое отражение, отличное от принятого по умолчанию:

glLightfv(GL LIGHTO, GL AMBIENT, @light ambient);

В последнем примере этой серии, проекте из подкаталога Ex14, модель тонирования задается значением, отличным от принятого по умолчанию:

glShadeModel (GL_FLAT);

На рис. 4. 8 показан получающийся результат - фигуры композиции потеряли гладкость своих форм.

Рис. 4. 8. Команда glShadeModel может существенно повлиять на получающиеся образы

Стоит сказать, что манипулирование свойствами источника света и оптическими свойствами материалов позволяет достичь весьма впечатляющих визуальных эффектов, поэтому давайте изучим данную тему максимально подробно.
В следующем простом примере, проекте из подкаталога Ех15, рисуется сфера с красивым бликом на поверхности. У источника света задается позиция, остальные параметры - по умолчанию. Материал определяется диффузной и зеркальной составляющими, размер блика описывается следующей строкой:

glMaterialf(GL_FRONT, GL_SHININESS, 25.0);

В этом примере есть то, чего мы ранее не встречали: здесь включается режим коррекции цвета материала и вызывается новая для нас команда:

glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT, GLJ3IFFUSE);

Команда glColorMaterial задает, какая из характеристик материала корректируется текущим цветом. Фактически это парная команда к режиму GL_COLOR_MATERIAL, однако в предыдущих примерах мы использовали значение, принятое по умолчанию.

Замечание
С точки зрения оптимизации эта команда предпочтительнее команды glMaterial.

В примере первоначальные значения элементов массива diffuseMaterial определяют диффузные свойства материала, по нажатию клавиш 'R', 'G' и 'В' увеличивается вес красного, зеленого или синего в текущем цвете:

procedure changeRedDiffuse;
begin
diffuseMaterial[0] := diffuseMaterial[0] + 0.1;
If diffuseMaterial[0] > 1.0
then diffuseMaterial[0] := 0.0;
glColor4fv(@diffuseMaterial);
end;

Проект из подкаталога Ex 16 представляет собой еще одну модель планетной системы: вокруг звезды вращается планета, вокруг которой вращается спутник (рис. 4.9).

Рис. 4.9. Наши астрономические модели становятся все более совершенными

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

const
sColor: array [0..3] of GLfloat = (1, 0.75, 0, 1);
black: array [0..3] of GLfloat = (0, 0, 0, 1);
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, @sColor);//излучение света
glutSolidSphere(0.8, 32, 16); // солнце
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, @black);// отключить

Благодаря этому наша звезда действительно светится.
На рис. 4.10 представлен результат работы следующего примера, проекта из подкаталога Ех17.

Рис. 4.10. Объекты сцены освещаются источниками различной фоновой интенсивности

Пример посвящен фоновой интенсивности света. Во-первых, задается полная интенсивность, как свойство источника света:

glLightModelfv(GL_LIGHT_MODEL_AMBIENT, @global_ambient);

Перед воспроизведением каждого чайника задается свое, локальное значение фоновой составляющей материала. Это значение минимально для верхнего объекта и максимально для нижнего.
В примере встречаем новую для нас команду:

glFrontFace (GL_CW);

В данном случае она никак не влияет на работу приложения, но дает нам повод подробнее изучить то, как задаются передняя и задняя стороны многоугольников (в предыдущих главах мы уже об этом немного говорили).
По умолчанию обход вершин против часовой стрелки задает лицевую сторону многоугольника. Вызов команды с аргументом GL_CW меняет порядок обхода на противоположный.
В проекте из подкаталога Ex18 с течением времени вращается квадрат, передняя сторона которого окрашена красным, задняя - синим. Если вершины перечислять в обратном порядке, квадрат развернется к наблюдателю задней стороной. To же самое произойдет, если нажать третью цифровую клавишу, в обработчике нажатия которой вызывается команда glFrontFace с аргументом GL_cw:

If Key = 49 then glEnable (GL_CULL_FACE); // нажата '1'
If Key = 50 then glDisable (GL_CULL_FACE); // нажата '2'
If Key = 51 then glFrontFace (GL_CCW); // нажата '3'
If Key = 52 then glFrontFace (GL_CW}; // нажата '4'
If Key = 53 then glCullFace (GL_FRONT); // нажата '5'
If Key = 54 then glCullFace (GL_BACK); // нажата '6'

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

If Key = 51 then begin
glFrontFace (GL_CCW); glNormal3f (0. 0, 0. 0, 1. 0);
end;
If Key = 52 then begin
glFrontFace (GL_CW);
glNorroal3f (0. 0, 0. 0, -1. 0);
end;

Переходим к следующему примеру, проекту из подкаталога Ex19. Здесь мы помимо того, что закрепим тему этого раздела, вспомним, как производить отсечение в пространстве.
На экране нарисованы три чайника, у каждого из них небольшая часть отсечена так, что можно заглянуть внутрь объекта (рис. 4. 11).

Рис. 4. 11. Можно заглянуть внутрь чайников

Вырезка осуществляется способом, знакомым нам по предыдущей главе:

glClipplane (GL_CLIP_PLANEO, @eqn);
glEnable (GL_CLIP_PLANEO);

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

glLightModelf (GL_LIGHT_MODEL_TWO_SIDE, 1);
glMaterialfv (GL_FRONT_AND_BACK, GL_DIFFUSE, @mat_diffuse);

Последний чайник отличается от предыдущего тем, что его внутренняя и Наружная поверхности покрыты разными материалами:

glMaterialfv (GL_FRONT, GL_DIFFUSE, @mat_diffuse);
glMaterialfv (GL_BACK, GL_DIFFUSE, @back diffuse);

Замечание
Для того чтобы увидеть внутренности объекта, необходимо следить, чтобы режим GL_CULL_FACE не был включен, иначе внутренние стороны многоугольников рисоваться не будут. В этом примере уже нельзя пренебречь вызовом glFrontFace, иначе второй и третий чайники будут вывернуты наизнанку.
Проект из подкаталога Ex20 является модификацией предыдущего примера, чайник здесь уже вращается и выглядит еще более живописным (рис 4.12)

Рис. 4. 12. Чайник снаружи изумрудный, внутри - золотой Такое увидишь только на экране монитора

Материалы для внутренней и внешней поверхностей чайника я взял из примера с двадцатью чайниками, приведенного выше.
Теперь мы изучили все, что необходимо для знакомства с классической программой, переложение которой на Delphi я поместил в подкаталог Ex2l Программа рисует группу из трех вращающихся шестеренок (рис. 4. 13)

Рис. 4. 13. Один из моментов работы проекта Gears

С помощью клавиш управления курсором и клавиши 'Z' можно изменять положение точки зрения в пространстве.
Для закрепления материала рассмотрим еще несколько примеров. Вы спокойно можете пропустить эту часть, если чувствуете, что предыдущих примеров оказалось достаточно для прочного овладения приемами работы с материалами.
Рис 4. 14 иллюстрирует работу программы - проекта из подкаталога Ex22, в которой происходят колебания и вращения системы из трех объектов.

Рис. 4. 14. Пример на колебания, объем фигур изменяется с помощью операции масштабирования

Первоначально объекты только колеблются, клавиша <Insert> позволяет управлять процессом вращения.
Для создания объектов системы используются функции библиотеки glu, quadric-объекты создаются перед описанием списков, после чего сразу же Удаляются.
Обратите внимание, что для колебательного изменения объема фигур используется масштабирование, задаваемое перед вызовом списков, в таких случаях нельзя забывать о включении режима пересчета нормалей:

9lEnable(GL_NORMALIZE);

Проект из подкаталога Ex23 является продолжением предыдущего примера, однако колебания системы здесь затухающие. Еще один несложный пример на свойства материала - проект из подкаталога Ex24 переносит все дальше в глубины космоса (рис. 4. 15).

Рис. 4.15. Пора признаться: я в детстве мечтал стать космонавтом

В этом примере все объекты сцены являются quadric-объектами.

Вывод на палитру в 256 цветов
В этом разделе я смогу вернуть некоторые свои долги, накопившиеся в предыдущих главах.
Ранее я обещал показать, почему наши самые первые примеры, где для упрощения не использовались операции с матрицей проекции, не годятся в качестве шаблона проектов.
Одну из таких программ я взял в качестве основы проекта из подкаталога Ех25, но вместо кубика на сцену поместил чайник.
Если при задании параметров вида не использовать ставшей привычной для нас последовательности операций, чайник выглядит на экране совсем невзрачным, будто вывернутым наизнанку.
Специально для этого случая приходится задавать модель освещенности с расчетом освещенности для обеих сторон многоугольников. Но и после этого тестовая фигура выглядит не очень эффектно.
Для тренировки я вставил строки с обычной последовательностью действий с матрицами проекций и матрицей моделирования, они первоначально закомментированы. Если вы уберете знаки комментария и удалите строки первого варианта, то тестовая фигура будет выглядеть на порядок эффектнее без каких-то дополнительных манипуляций:

procedure TfrmGL.FormResize(Sender: TObject);
begin glViewport(0, 0, ClientWidth, ClientHeight);
// один вариант
{--------------------}
glLoadldentity;
glFrustumf(1, 1, -1, 1, 5, 10) ;
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
{-------------}

// второй вариант
{ --------------------}
{ glMatrixMode(GL_PROJECTION);
glLoadldentity;
glFrustum (-1, 1, -1, 1, 5, 10);
glMatrixMode(GL_MODELVIEW);
glLoadldentity;
{-------------------------}

glTranslatef(0.0, 0.0, -8.0); // перенос объекта - ось Z
glRotatef(30.0, 1.0, 0.0, 0.0); // поворот объекта - ось X
glRotatef(70.0, 0.0, 1.0, 0.0); // поворот объекта - ось Y

InvalidateRect(Handle, nil, False);
end;

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

Замечание
Если вы собираетесь распространять свои приложения, то я вам рекомендую позаботиться и о тех пользователях, которые по каким-либо причинам используют только 256 цветов. Качество воспроизведения, конечно, при этом страдает, но это все же лучше, чем ничего.

Проект из подкаталога Ех26 иллюстрирует, какие действия необходимо предпринять, если формат пиксела содержит установленный флаг, указывающий на необходимость корректировки палитры (PFD_NEED PALETTE).
Код я снабдил комментариями, с помощью которых вы при наличии желания сможете в нем разобраться. Не будем подробно рассматривать производимые манипуляции, поскольку эта тема не относится непосредственно к библиотеке OpenGL. Замечу только, что обработку сообщений, связанных с изменением системной палитры, в принципе можно и не производить, Делается это для корректности работы приложения.

Подробнее о поверхностях произвольной формы
В предыдущей главе уже были рассмотрены несколько примеров построения поверхности произвольной формы. В этом разделе мы продолжим этот разговор.
На рис. 4.16 представлен результат работы программы - проекта Ех27, моей модификации широко известной программы isosurf.c.

Рис. 4.16. Для построения поверхности используется более семи тысяч точек, поверхность строится как группа связанных треугольников

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

procedure DrawSurface;
var
i: GLuint;
begin
glBeginf GL_TRIANGLE_STRIP);
For i := 0 to numverts - 1 do begin glNormal3fv( @norms[i]);
glVertex3fv( @verts[i]);
end;
glEnd;
end;

Клавишами управления курсором можно вращать фигуру, есть режим тестирования скорости воспроизведения, активизируемый нажатием клавиши 'Т'' фигура делает полный оборот по оси X.
Программа, несмотря на обширность кода, проста и не должна вызвать особых затруднений при разборе. Стоит только обратить внимание, что на сцене присутствует два источника света, различающиеся позицией.
Этот пример демонстрирует, что, в принципе, для воспроизведения поверхностей сложной формы не обязательно пользоваться сплайнами, а можно разбить поверхность на множество простых примитивов.
Следующий пример окажется очень полезным для тех, кто настроен на профессиональную работу с OpenGL.
В проекте из подкаталога Ех24 координаты вершин треугольников, образующих поверхность, считываются из файла формата dxf (рис. 4.17).

Рис. 4.17. Для построения поверхности используется

Теперь вы можете подготавливать модели с помощью любого профессионального редактора - формат dxf является открытым и стандартным.

Замечание
Саму поверхность не обязательно строить по отдельным треугольникам, главное, чтобы используемый редактор мог разбивать поверхность на такие треугольники при экспорте в dxf-формат.

Я соглашусь с вами, что некоторые примеры, хоть и являются очень интересными, годятся только для того, чтобы пару раз ими полюбоваться. Этот же пример является действительно полезным и достоин подробного разбора.
Программа является универсальной, в частности, в ней не ограничивается число точек поверхности. В таких случаях можно использовать динамические массивы или, как в этом Примере, списки (в терминах Delphi).
Списки введены в программе для хранения точек модели и нормалей к каждому треугольнику:

Model, Normals : TList;

Следующий тип введен для хранения координат отдельной точки:

type
Vector = record
х, у, z : GLfloat;
end;

Одной из ключевых процедур примера является процедура чтения данных из файла формата dxf:

I$WARNINGS OFF} // отключить предупреждения компилятора о возможной
// неинициализации переменных
procedure TfrmGL.LoadDXF (st : String);
var
f : TextFile;
wrkString : String;
group, err : GLint;
x1, x2, y1, y2, z1, z2, x3, y3, z3 : GLfloat;
// вспомогательная процедура добавления вектора в список
Model procedure AddToList (х, у, z : GLfloat);
var
wrkVector : Vector; // рабочая переменная, вектор
pwrkVector : PVector; // указатель на вектор begin
wrkVector.х := х; // заполняем поля вектора
wrkVector.у := у; wrkVector.z := z;
New (pwrkVector); // выделение памяти для нового элемента списка
pwrkVectorA := wrkVector; // задаем указатель
Model.Add (pwrkVector); // собственно добавление вектора в список
end;
begin
AssignFile(f,st); // открываем файл
Reset(f);
repeat // пропускаем файл до секции объектов "ENTITIES"
ReadLn(f, wrkString);
until (wrkString = 'ENTITIES') or eof(f);
While not eof (f) do begin
ReadLn (f, group); // маркер
ReadLn (f, wrkString); // идентификатор либо координата
case group of
0: begin // начался следующий объект
AddToList (х3, у3, z3); // добавляем в список треугольник
AddToList (x2, у2, 2.2); AddToList (x1, y1, z1);
end;
10: val(wrkString, x1, err) // считываем вершины треугольника
20: val(wrkString, y1, err) 30: val(wrkString, z1, err)
11: val(wrkString, x2, err) 21: val(wrkString, y2, err)
31: val(wrkString, z2, err) 12: val(wrkString, x3, err)
22: val(wrkString, y3, err) 32: val(wrkString, z3, err)
end;
end;
CloseFile(f);
end;
{$WARNINGS ON}

После того как считаны координаты вершин треугольников, образующих поверхность, необходимо рассчитать векторы нормалей к каждому треугольнику:

($HINTS OFF) // отключаем замечания компилятора о неиспользовании
// переменной pwrkVector procedure TfrmGL.CalcNormals;
var
i : Integer;
wrk1, vx1, vy1, vz1, vx2, vy2, vz2 : GLfloat;
nx, ny, nz : GLfloat;
wrkVector : Vector;
pwrkVector : ^Vector;
wrkVector1, wrkVector2, wrkVectorS : Vector;
pwrkVector1, pwrkVector2, pwrkVector3 : ^Vector; begin
New (pwrkVector1); // выделение памяти под указатели
New (pwrkVector2);
New (pwrkVector3);
For i := 0 to round (Model.Count / 3) - 1 do begin
pwrkVector1 := Model [i * 3]; // считываем по три вершины из списка
wrkVector1 := pwrkVector1^; // модели
pwrkVector2 := Model [i * 3 -t- 1] ;
wrkVector2 := pwrkVector2^;
pwrkVector3 := Model [i * 3 + 2];
wrkVector3 := pwrkVectorS";
// приращения координат вершин по осям
vx1 := wrkVector1.х - wrkVector2.x;
vy1 := wrkVector1.y - wrkVector2.у;
vz1 := wrkVectorl.z - wrkVector2.z; 1
vx2 := wrkVector2.x - wrkVector3.x; 1
vy2 := wrkVector2.y - wrkVector3.y;
vz2 := wrkVector2.z - wrkVector3.z;
// вектор-перпендикуляр к центру треугольника
nx := vy1 * vz2 - vz1 * vy2;
ny := vz1 * vx2 - vx1 * vz2;
nz := vx1 * vy2 - vy1 * vx2; // получаем унитарный вектор единичной длины
wrk1 := sqrt (nx * nx + ny * ny + nz * nz);
If wrk1 = 0 then wrk1 := 1; // для предотвращения деления на ноль
wrkVector.x := nx / wrk1;
wrkVector.y := ny / wrk1;
wrkVector.z := nz / wrk1;
New (pwrkVector); // указатель на очередную нормаль
pwrkVector4 := wrkVector;
Normals.Add (pwrkVector); // добавляем нормаль в список Normals
end;
end;
{$HINTS ON}

Собственно поверхность описывается в дисплейном списке:

Model := TList.Create; // создаем список модели
Normals := TList.Create; // создаем список нормалей
LoadDxf ('Dolphin.dxf'); // считываем модель
CalcNormals; // расчет нормалей
glNewList (SURFACE, GL_COMPILE); // поверхность хранится в списке
For i := 0 to round (Model.Count / 3) - 1 do begin // по три вершины
glBegin(GL_TRIANGLES);
glNormal3fv(Normals.Items [i]); // задаем текущую нормаль
glvertex3fv(Model.Items [i * 3]); // вершины треугольника
glvertex3fv(Model.Items [i * 3 + 1]); glvertex3fv(Model.Items [i * 3 + 2]);
glEnd;
end;
glEndList;
Model.Free; // списки больше не нужны, удаляем их
Normals.Free;

Замечание
Я не мог привести этот пример в предыдущей главе, поскольку в этом примере необходимо задать модель освещения с расчетом обеих сторон многоугольников у нас нет гарантии, что все треугольники поверхности повернуты к наблюдателю лицевой стороной

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

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

Использование патчей
Одним из главных достоинств сплайнов является то, что поверхность задается небольшим количеством опорных точек - весьма экономный подход

Замечание
Возможно, к достоинствам можно отнести и то, что нет необходимости самостоятельно рассчитывать нормали к поверхности, как это делалось в предыдущем примере

Вспомним пример предыдущей главы с холмообразной поверхностью Для хранения этой поверхности достаточно запомнить шестнадцать точек, если ее разбивать по треугольникам, то шестнадцати вершин окажется явно не достаточно для сохранения гладкости.
Подкаталог Ех29 содержит модификацию примера предыдущей главы на построение NURBS-поверхности. Отличает этот проект то, что в нем задаются свойства материала, так что поверхность выглядит гораздо эффектнее, также здесь добавилось то, что среди опорных точек одна выделяется и Рисуется красным цветом. Такую точку можно перемещать в пространстве нажимая на клавиши 'X', Т, 'Z', а также на эти клавиши совместно с <Shift>. Клавишами управления курсором можно задавать, какая из опорных точек является выделенной. Создавать кусочки поверхностей с помощью сплайнов - дело несложное, если попытаться подобрать опорные точки для того, чтобы нарисовать одним сплайном поверхности подобные чайнику, то это окажется занятием крайне трудоемким. Если же поверхность имеет негладкие участки, как, например, область соприкосновения носика чайника с его корпусом, то традиционными сплайнами это нарисовать окажется вообще невозможно. Имеется стандартный способ рисования поверхностей, сочетающий в себе достоинства использования сплайнов и лишенный его недостатков: поверхность разбивается на отдельные гладкие участки, для каждого из которых строится отдельный сплайн. Такие кусочки называются патчами (patch, заплатка).
В частности, модель чайника строится именно таким способом, по отдельным патчам, вы можете заглянуть в модуль для того, чтобы убедиться в этом. Следующий пример, проект из подкаталога Ex25, строит поверхность на основе патчей, опорные точки для которых считываются из текстового файла (рис. 4. 19).

Рис. 4. 19. Еще одна модель из отдельных кусочков

Программа написана мною, но автором модели являюсь не я, а Геннадий Обухов, адрес его страницы я указал в приложении 1.
Модель состоит из 444 патча размером 4x4, по шестнадцать точек. Для хранения данных о патчах введен пользовательский тип:

type
TVector = record
x, у, z: GLfloat; end;
TPatch = Array [0.. 15] of TVector;

Модель так же, как и в предыдущем примере, хранится в списке. Список заполняется данными, считываемыми из текстового файла, каждая строка которого содержит координаты очередной вершины. Эта программа тоже является в некотором роде универсальной: требование к файлу заключается в том, что точки должны записываться порциями по шестнадцать, каждая группа представляет собой опорные точки очередного патча.
Я не могу поместить на дискету дополнительные примеры к этой программе, текстовые файлы слишком велики, поясню только, как создаются подобные файлы данных.
Я использовал свободно распространяемый модельер sPatch, позволяющий использовать встраиваемые модули (plug-in) для экспортирования моделей, созданных в нем из патчей, в произвольный формат. В частности, этот модельер позволяет записывать и в формате dxf. Адрес, по которому можно получить sPatch, указан в приложении 1.
Ключевой процедурой разбираемого примера является процедура инициализации поверхности:

Procedure TfrmGL. Init_Surface;
var
f: TextFile;
1: Integer;
Model: TList; // список модели
wrkPatch: TPatch; // вспомогательная переменная, патч
PwrkPatch: ^TPatch; // указатель на патч 188
begin
Model := TList.Create; // создание списка модели
AssignFile (f, 'Parrot.txt'); // открытие файла
ReSet (f);
While not eof (f) do begin
For i := 0 to 15 do // точки считываются по шестнадцать
ReadLn (f, wrkPatch [i].x, wrkPatch [i].y, wrkPatch [i].z);
New (pwrkPatch); // выделение памяти под очередной элемент
pwrkPatch^ := wrkPatch, // задаем указатель на элемент
Model.Add (pwrkPatch); // собственно добавление в список
end;
CloseFile (f);
glNewList (SURFACE, GL_COMPILE);
glPushMatrix; // матрица запоминается из-за масштабирования
glScalef (2.5, 2.5, 2.5) ;
For i := 0 to Model.Count - 1 do begin // цикл построения патчей
glMap2f(GL_MAP2_VERTEX_3, 0, I, 3, 4, 0, 1, 12, 4, Model.Items[i]);
glEvalMesh2(GL_FILL, 0, 4, 0, 4);
end;
glPopMatrix;
glEndList;
Model.Free; // удаление списка
end;

Закончу рассмотрение примера двумя замечаниями:

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

Замечание
Несмотря на то, что при высоком уровне детализации разбиения поверхности на отдельные треугольники мы получим поверхность, не уступающую по качеству воспроизведения варианту со сплайнами, я настоятельно посоветую вам использовать все-таки поверхность Безье Высокая детализация приведет к огромному количеству воспроизводимых примитивов, и скорость воспроизведения заметно упадет.

Приведу еще один прекрасный пример на использование патчей, проект из подкаталога Ех31 (рис 4.20).

Рис. 4.20. Этот пример посвящается всем девушкам-программисткам

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

glNewList (ROZA, GL_COMPILE);
glPushMatrix;
glScalef (0.5, 0.5, 0.5);
For i := 0 to 11 do begin // первые 12 патчей - лепестки
glColorSf (1.0, 0.0, 0.0); // задаем цвет красным
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 5, О, 1, 15, 5, Model.Items[i]);
glEvalMesh2(GL_FILL, 0, 20, 0, 20);
end,
For i := 12 to Model.Count - 1 do begin // стебель цветка
glColor3f (0.0, 1.0, 0.0); // цвет - зеленый
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 5, 0, I, 15, 5, Model.Items[i]);
glEvalMesh2(GL_FILL, 0, 20, 0, 20);
end;
glPopMatrix;
glEndList;

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

Буфер трафарета
Многие специальные эффекты, самым простым из которых является вырезка объекта, основаны на использовании буфера трафарета (шаблона).

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

Выполнение теста трафарета разрешается командой glEnable с аргументом GL_STENCIL_TEST
Команда glstencilFunc отвечает за сравнение, а команда glstencilop позволяет определить действия, базирующиеся на результате проверки трафарета Другие команды, связанные с буфером трафарета, рассмотрим по ходу изучения примеров, первым из которых станет проект из подкаталога Ех32, результат работы которого представлен на рис 4.21

Рис. 4.21. Простейший пример на операции с буфером трафарета

Инициализация работы приложения начинается с задания характеристик буфера трафарета

glClearStencil(0); // значение заполнения буфера трафарета при очистке
glStencilMask(l); // число битов, задающее маску
glEnable(GL_STENCIL_TEST); // включаем тест буфера трафарета

Первые две команды вызываются с аргументами, имеющими значение, принятое по умолчанию, поэтому могут быть удалены

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

Первая команда задает фоновое значение, которым будет заполнен буфер при выполнении команды glclear с аргументом GL_STENCIL_BUFFER_BIT вторая команда разрешает или запрещает перезапись битов в плоскости трафарета
Теперь разберем код перерисовки кадра:

// очищаются буфер цвета и буфер трафарета
glClearf GL_COLOR_BUFFER_BIT or GL_STENCIL_BUFFER_BIT);
// треугольник
// тест всегда завершается положительно
glstencilFunc(GL_ALWAYS, 1, I);
// значение буфера устанавливается в 1 для всех точек треугольника
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glColor3ub(200, 0, 0); // цвет - красный
glBegin(GL_POLYGON);
glVertex3i(-4, -4, 0);
glVertex3i( 4, -4, 0);
glVertex3i( 0, 4, 0) ;
glEnd;
// зеленый квадрат
// только для точек, где в буфере записана единица
glStencilFunc(GL_EQUAL, 1, 1),
// для точек, не подпадающих в тест, значение буфера установить в
// максимальное значение;
// если тест завершился удачно, но в буфере глубины меньшее значение,
// сохранить текущее значение буфера трафарета;
// если в буфере глубины большее значение, задать значение нулевым
glStencilOp(GL_INCR, GLJKEEP, GL_DECR);
glColor3ub(0, 200, 0); glBegin(GL_POLYGON);
glVertex3i(3, 3, 0};
glVertex3i(-3, 3, 0);
glVertex3i(-3, -3, 0);
glVertex3i(3, -3, 0); glEnd;
// синий квадрат
// только для точек, где в буфере записана единица
glstencilFunc(GL_EQUAL, I, 1);
// для всех точек сохранить текущее значение в буфере трафарета
glStencilOp (GLJCEEP, GL_KEEP, GL_KEEP) ;
glColor3ub(0, 0, 200);
glBegin(GL_POLYGON);
glVertex3i(3, 3, 0) ;
glVertex3i(-3, 3, 0);
glVertex3i(-3, -3, 0);
glVertex3i(3, -3, 0);
glEnd;

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

glStencilOp(GL_INCR, GL_KEEP, GLJ3ECR);

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

Замечание
Наверное этот раздел покажется поначалу сложным для понимания но хочу вас успокоить тем, что совсем не обязательно сразу же уяснить все тонкости работы с буфером трафарета Вы можете пока пробежаться по примерам чтобы затем вернуться к ним в случае необходимости и еще раз хорошенько все повторить. Сейчас мы попробуем решить одну и ту же задачу двумя немного различными способами Задача состоит в том, чтобы проделать отверстие в квадратной площадке.
На рис 4.22 представлен результат работы проекта из подкаталога Ех33 квадратная площадка с просверленным отверстием в виде кольца (на экране она вращается), сквозь которое видно красную сферу, расположенную позади площадки.

Рис. 4.22. Отверстие в квадрате сделано с помощью буфера трафарета

Замечание
В таких примерах обычно не используют буфер глубины Замечу, что если необходимо на время отключать этот буфер можно воспользоваться командой glDepthMask c аргументом False

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

glClear(GL_COLOR_BUFFER_BIT or GL_STENCIL_BUFFER BIT);
glPushMatrix;
If fRot then glRotatef(theta, 1.0, I 0, 0.0); // поворот площадки
glColor3f(l 0, 1.0, 0.0); // площадка желтого цвета
glStencilFunc(GL_ALWAYS, 1, 1); // площадка рисуется всегда
// из-за GL_ALWAYS первый аргумент безразличен
// второй аргумент безразличен, поскольку не используется буфер глубины
glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
glBegin(GL_QUADS); // собственно площадка
glNormal3f(0.0, 0.0, 1.0);
glVertexSf(0.0, 0.0, 0.0);
glVertex3f(100.0, 00, 00);
glVercexSf(100.0, 100.0, 0.0);
glVertex3f(0.0, 100.0, 0.0);
glEnd;
// отверстие 9lPushMatrix;
glTranslatef(50.0, 50.0, 0.0); // в центр площадки
glStencilFunc(GL_NEVER, 1, 1); // площадка не рисуется никогда
// важен только первый аргумент, любое значение
// кроме GL_KEEP и GL_REPLACE
glStencilOp(GL_DECR, GL_REPLACE, GL_REPLACE);
gluDisk(qObj, 10. 0, 20. 0, 20, 20); // диск отверстия
glPopMatrix;
glPopMatrix;
// вернулись в первоначальную систему координат, сфера не вращается
glPushMatrix;
glColor3f(1. 0, 0. 0, 0. 0); // сфера красного цвета
glTranslatef (45. 0, 40. 0, -150. 0);
// рисовать только там, где присутствует только фон
glStencilFunc (GL_NOTEQUAL, 1, 1);
// важен только первый аргумент
glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
gluSphere (qObj, 50. 0, 20, 20); // собственно сфера
glPopMatrix;

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

Рис. 4. 23. Теперь просверлены два отверстия, обратите внимание на их пересечения

Теперь в квадрате просверлены два отверстия, пересекающиеся друг с другом, причем области пересечения дырок заполнены материалом Параметры операций с буфером трафарета задаются один раз - при инициализации работы приложения:

glStencilFunc(GL_EQUAL, 0, 1);
// в следующей команде важны первый и третий аргументы
glStencilOp(GL_INCR, GL_INCR, GL_INCR);
glEnable(GL_STENCIL_TEST); // разрешаем тест трафарета

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

glClear(GL_COLOR_BUFFER_BIT or GL_STENCIL_BUFFER_BIT);
glPushMatrix;
glPushMatrix;
glColor3f(1. 0, 0. 0, 0. 0); // сфера красного цвета
glTranslatef (45. 0, 40. 0, -150. 0);
gluSphere (qObj, 50. 0, 20, 20);
glPopMatrix;
If fRot then glRotatef(theta, 1. 0, 1. 0, 0. 0); // поворот площадки
glColorMask(False, False, False, False); // отключить работу с цветом
glPushMatrix;
// первая дырка
glTranslatef(45. 0, 45. 0, 0. 0);
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
// вторая дырка
glTranslatef(20. 0, 20. 0, 0. 0);
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
glPopMatrix;
glColorMask(True, True, True, True); // включить работу с цветом
glColor3f(1. 0, 1. 0, 0. 0); // задаем цвет желтым
// площадка
glBegin(GL_QUADS);
glNormal3f(0. 0, 0. 0, 1. 0);
glVertex3f(0. 0, 0. 0, 0. 0);
glVertex3f(100. 0, 0. 0, 0. 0);
glVertex3f(100. 0, 100. 0, 0. 0);
glVertex3f(0. 0, 100. 0, 0. 0);
glEnd;
glPopMatrix;

Смысл примера состоит в том, что для выделяемой области, т. e области отверстий, мы заносим в буфер кадра значение, соответствующее цвету фона Задание маски цвета из четырех отрицаний соответствует тому, что ни один компонент палитры цвета не будет записываться для области отверстий - отключается работа с буфером кадра Этот прием мы часто будем использовать.
Можно и не использовать буфер трафарета, а основываться только на буфе-Ре кадра и буфере глубины. Проект из подкаталога Ex35 содержит пример, функционирующий подобно предыдущему, но не использующий буфер шаблона.
Каждое отверстие рисуется два раза, чуть ниже и чуть выше площадки, но на время их воспроизведения отключается работа с буфером цвета:

glColorMask(False, False, False, False); // отключается работа с цветом
glPushMatrix;
// первая дырка
glTranslatef(45. 0, 45. 0, 0. 01); // над площадкой
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
glTranslatef(0. 0, 0. 0, -0. 02); // под площадкой
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
// вторая дырка
glTranslatef(20. 0, 20. 0, 0. 02); // над площадкой
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
glTranslatef(0. 0, 0. 0, -0. 02); // под площадкой
gluDisk(qObj, 15. 0, 20. 0, 20, 20);
glPopMatrix;
glColorMask(True, True, True, True); // возвращается работа с цветом
glColor3f(1. 0, 1. 0, 0. 0);
// площадка желтого цвета
glBegin(GL_QUADS);
glNormal3f(0. 0, 0. 0, 1. 0);
glVertex3f(0. 0, 0. 0, 0. 0);
glVertex3f(100. 0, 0. 0, 0. 0);
glVertex3f(100. 0, 100. 0, 0. 0);
glVertex3f(0. 0, 100. 0, 0. 0);
glEnd;
glPopMatrix;

Отверстия приходится рисовать по два раза потому, что иначе через них местами проглядывает площадка.
Я уже однажды говорил об этом, но сейчас еще раз напомню в рассматриваемых примерах на сцене присутствует не больше пяти объектов, поэтому не заметно, насколько сильно тормозит приложение интенсивная работа с буферами Если требуется только прорезать отверстия в поверхностях, эффективнее самостоятельно разбить поверхность на отдельные зоны и нарисовать дырку подобно тому, как это делалось в примерах главы 2. В главе 5 мы рассмотрим еще один пример того, как создать дырявую поверхность.
Следующий пример, проект из подкаталога Ех30, относится к разряду классических, в работе над ним моя заслуга состоит только в переносе программы на Delphi.
Рис. 4. 24 иллюстрирует работу приложения - на экране два тора, в середине экрана прорезано квадратное отверстие, сквозь которое проглядывает синяя сфера.

Рис. 4. 24. При изменении размеров окна программы получается очень интересный эффект

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

Procedure TfrmGL. FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glClear(GL_STENCIL_BUFFER_BIT); // очищаем буфер трафарета
glMatrixMode(GL_PROJECTION);
glLoadldentity;
g!0rtho(-3.0, 3.0, -3.0, 3.0, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadldentity;
// создаем квадрат посередине сцены
glStencilFunc (GL_ALWAYS, $1, $1);
glStencilOp (GL_REPLACE, GL_REPLACE, GL_REPLACE);
glBegin(GL_QUADS);
glVertex3f (-1.0, 0.0, 0.0);
glVertex3f (0.0, 1.0, 0.0);
glVertex3f (1.0, 0.0, 0.0);
glVertex3f (0.0, -1.0, 0.0);
glEnd;
// переопределяем видовые параметры
glMatrixMode(GL_PROJECTION);
glLoadldentity;
gluPerspective(45.0, ClientWidth / ClientHeight, 3.0, 7.0);
glMatrixMode(GL_MODELVIEW);
glLoadldentity;
glTranslatef(0.0, 0.0, -5.0);
InvalidateRect(Handle, nil, False);
end;

Видовые параметры переопределяются по ходу обработки события. Вы можете вставить showMessage в этот обработчик, чтобы создать паузу и увидеть, как заданы видовые параметры в первом и во втором случаях. Квадрат строится в "мировой" системе координат, затем видовые параметры берут за основу размеры экрана. Поэтому при изменениях размера окна торы не меняют пропорций, а квадрат принимает форму ромба.

Замечание
Обращаю ваше внимание на то, что квадрат "рисуется" не в буфере кадра, т е на экране, а в буфере трафарета.

Теперь перейдем к собственно воспроизведению:

// рисуем синюю сферу там, где значение буфера трафарета равно 1
glStencilFunc (GLJ2QUAL, $1, $1);
glCallList (BLUEMAT);
glutSolidSphere (0.5, 20, 20);
// рисуем желтые торы там, где значение буфера трафарета не равно 1
glStencilFunc (GL_NOTEQUAL, $1, $1);
glStencilOp (GL_KEEP, GL_KEEP, GL_KEEP);
glPushMatrix;
glRotatef (45.0, 0.0, 0.0, 1.0);
glRotatef (45.0, 0.0, 1.0, 0.0);
glCallList (YELLOWMAT);
glutSolidTorus (0.275, 0.85, 20, 20);
glPushMatrix;
glRotatef (90.0, 1.0, 0.0, 0.0);
glutSolidTorus (0.275, 0.85, 20, 20);
glPopMatrix;
glPopMatrix;

Поскольку параметры glstencilop не изменились перед воспроизведением синей сферы, рисуются все ее участки, но в области, пересекающейся с вырезанным квадратом, мы видим лицевую сторону сферы, а в остальных ее участках рисуется задняя поверхность. Пример простой, но очень изящный, рекомендую вам разобраться в нем основательно.
Рис. 4.25 иллюстрирует работу следующего, тоже весьма интересного примера, проекта из подкаталога Ех37.

Рис. 4.25. Это один из самых интересных примеров

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

type
proctype = procedure;
var
а: proctype = procCube;
b: proctype = procSphere;

Логическая операция OR заключается в том, что воспроизводятся обе фигуры

procedure procOR (a, b: proctype);
begin
glPushAttrib(GL_ALL_ATTRIB_BITS);
glEnable(GL_DEPTH_TEST);
а;
b;
glPopAttrib;
end;

Для получения части фигуры А, находящейся внутри В, предназначена следующая процедура:

procedure inside(a, b: proctype; face, test: GLenum);
begin
// рисуем А в буфере глубины, но не в буфере кадра
glEnable(GL_DEPTH_TEST);
glColorMask(FALSE, FALSE, FALSE, FALSE};
glCullFace(face);
а;
// буфер трафарета используется для нахождения части А, находящейся
// внутри В. Во-первых, увеличиваем буфер трафарета для передней
// поверхности В.
glDepthMask{FALSE);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 0, 0);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glCullFace(GL_BACK); // отсекаем заднюю часть В
b;
// затем уменьшаем буфер трафарета для задней поверхности
glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
glCullFace(GL_FRONT); // отсекаем переднюю часть В
b;
// теперь рисуем часть фигуры А, находящуюся внутри В
glDepthMask(TRUE);
glColorMask(TRUE, TRUE, TRUE, TRUE);
glStencilFunc(test, 0, 1);
glDisable(GL_DEPTH_TEST);
glCullFace(face);
glDisable(GL_STENCIL_TEST); // отключаем буфер трафарета
end;

Вспомогательная процедура fixup предназначена для регулирования содержимого буфера глубины, фигура выводится только в этот буфер:

procedure fixup (а: proctype);
begin
glColorMask(FALSE, FALSE, FALSE, FALSE); // отключаем вывод в кадр
glEnable(GL_DEPTH_TEST); // включаем тестирование глубины
glDisable(GL_STENCIL_TEST); // отключаем операции с трафаретом
glDepthFunc(GL_ALWAYS); // все точки фигуры - в буфер глубины а;
// рисуем фигуру только в буфер глубины
glDepthFunc(GL_LESS); // отключаем буфер глубины
end;

Логическая операция AND состоит в нахождении пересечения двух фигур: находим часть А, находящуюся внутри В, затем находим часть В, находящуюся внутри А:

procedure procAND (a, b: proctype);
begin
inside(a, b, GL_BACK, GL_NOTEQUAL);
fixup(b); // рисуем фигуру В в буфер глубины
inside(b, a, GL_BACK, GL_NOTEQUAL);
end;

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

procedure sub (a, b: proctype);
begin
inside(a, b, GL_FRONT, GL_NOTEQUAL);
fixup(b);
inside(b, a, GL_BACK, GL_EQUAL);
end;

Клавиши управления курсором предназначены для перемещения фигур в пространстве, регистровые клавиши <Shift> и <Alt> используются для выбора перемещаемой фигуры. Клавиша 'Z' ответственна за приближение точки зрения, набор фигур меняется при нажатии 'C' Здесь обратите внимание на то, как анализируется значение переменных процедурного типа:

If (@A = @procCube) and (@B = @procSphere) then begin
A: = procSphere;
В: = procCone;
end
else begin If (@A = SprocSphere) and (@B = @procCone) then begin
A := procCone;
В .= procCube,
end
else begin
A o= procCube;
В := procSphere;
end
end;

Думаю, этот пример окажется очень полезным для многих читателей
Рис 4.26 демонстрирует результат работы еще одного примера на буфер трафарета, проекта из подкаталога Ех38, в котором красиво переливается поверхность додекаэдра с приклеенной буквой "Т".

Рис. 4.26. С помощью буфера трафарета легко получать узоры на поверхности

Смешение цветов и прозрачность
Так называемый альфа-компонент, задаваемый четвертым аргументом в командах, связанных с цветом фона и примитивов, используется для получения смеси цветов
Режим смешения включается командой glEnable с аргументом GL_BIEND Помимо этой команды необходимо задать пиксельную арифметику с помощью glBlendFunc Аргументы этой команды определяют правила, по которым происходит смешение поступающих значений RGBA с содержимым буфера кадра.

Рис. 4.27. Простейший пример на смешение цветов

Как обычно, разобраться нам помогут примеры. Начнем с проекта из подкаталога Ех39, где на экране строятся два частично перекрывающихся треугольника (рис 4.27)

Диалог с OpenGL начинается с установки параметров смешивания:

glEnable (GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ;

Нет смысла приводить здесь перечень и смысл аргументов команды glBlendFunc, справка по этой команде достаточно подробна Там, в частности, говорится и о том, что прозрачность наилучшим образом осуществляется при использовании значений аргументов GL_SRC_ALPHA и GL_ONE_MINUS SRC_ALPHA, как сделано в рассматриваемом примере
Для обоих треугольников значение альфа установлено одинаковым, 0 75 Чем больше это число, тем ярче рисуются примитивы
Нажимая на клавишу Т, можно менять порядок построения треугольников, нарисованный первым располагается ниже и проглядывает сквозь следующий. Проект из подкаталога Ех40 во многом похож на предыдущий, только здесь нарисовано четыре фигуры, три прямоугольника и один квадрат В примере Дважды накладываются примитивы одной пары цветов, но в разном порядке, из-за чего различаются оттенки получающихся смесей. Следующие два примера также очень похожи друг на друга.В проекте из подкаталога Ех41 на сцене присутствуют два объекта непрозрачная сфера и полупрозрачный куб (рис 4 28).

Рис. 4.28. Эффект полупрозрачности сквозь куб просматривается сфера заднего плана

Первоначально сфера находится на переднем плане, после нажатия клавиши А' сфера и куб меняются местами
При задании оптических свойств материала для сферы альфа-составляющая Устанавливается единичной, для куба - 06, поэтому куб выглядит полупрозрачным, а сфера - сплошной 204
В проекте из подкаталога Ех42 рисуются полупрозрачный цилиндр и непрозрачный тор, при щелчке кнопкой мыши меняется точка зрения. Принцип, используемый в этом проекте для получения эффекта полупрозрачности, ничем не отличается от приема предыдущего примера.
Следующий пример, располагающийся в подкаталоге Ех43, продолжает тему. На экране располагаются две сферы, красная вложена внутрь совершенно прозрачной, наружная сфера со временем мутнеет.
Пример очень простой: в обработчике таймера увеличивается значение переменной transparent, режим смешения цветов включается один раз в самом начале работы, а при воспроизведении кадра режим не переключается'

(красная сфера внутри}
glColorSf(1.0, 0.0, 0.0); // по умолчанию альфа =1.0
gluSpheref(qobj, 0.75, 20, 20);
{наружная сфера}
glColor4f(1.0, 1.0, 1.0, transparent);
gluSphere (qObj, 1.0, 20, 20);

Дальше нам следует разобрать проект из подкаталога Ех44, очень похожий на один из ранее разобранных примеров, в котором рисовался фонтан точек. Теперь точки стали полупрозрачными (рис. 4.29).

Рис. 4.29. Фонтан точек, точки полупрозрачны

Окно снабжено всплывающим меню, один из пунктов которого позволяет отключить режим смешения. Можете сравнить результат
Режим смешения часто используется для сглаживания краев объектов. Проект из подкаталога Ех45 демонстрирует эту возможность на примере точек, которые выводятся со сглаженными, нерезкими, краями.
Помимо собственно режима сглаживания точек, в программе также включается режим смешения цветов:

glEnable (GL_POINT_SMOOTH);
glEnable (GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint (GL_POINT_SMOOTH_HINT, GL_DONT_CARE);
glPointSize (3.0);

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

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

glGetFloatv (GL_LINE_WIDTH_GRANULARITY, Svalues);
ShowMessage(Format ('GL_LINE_WIDTH_GRANULARITY value is %3.lf,
[values[0]]));
glGetFloatv (GL_LINE_WIDTH_RANGE, @values);
ShowMessage(Format ('GL_LINE_WIDTH_RANGE values are %3.1f %3.lf,
[values[0], values[1]])); glEnable (GL_LINE_SMOOTH);
glEnable (GL_BLEND);
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glHint (GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
glLineWidth (1.5);

Теперь решим несколько несложных задач по теме альфа-смешивания. Рис. 4. 30 показывает картинку работы приложения, полученного после компиляции проекта из подкаталога Ex48, где полупрозрачная сфера вращается вокруг непрозрачного конуса.

Рис. 4. 30. При рисовании полупрозрачных замкнутых объектов появляются ненужные узоры

Реализовано все тривиально - у всех объектов, присутствующих на сцене, оптические свойства материала заданы с единичным значением альфа-компонента, для сферы это значение равно 0. 5. При вращении сферы на ее поверхности появляется узор, который портит всю картину. Эта проблема очень распространена, и новички часто натыкаются на нее. Связана она с наложением передней и задней полупрозрачных поверхностей сферы и вообще любой замкнутой полупрозрачной поверхности.
В этом разделе уже был пример с полупрозрачной сферой, где такой проблемы не возникало, но там сфера не вращалась, и мы всегда наблюдали ее с выгодной точки зрения. Если вы посмотрите проект из подкаталога Ex49, в котором та же полупрозрачная сфера вращается по всем осям, то обнаружите, что в некоторых положениях появляется паразитный узор.
Самым простым решением будет не воспроизводить внутреннюю часть сферы, но если по сценарию внутренняя поверхность изготовлена из другого материала, мы этого не увидим. To есть сфера не получится действительно полупрозрачной, как бы изготовленной из матового стекла. В проекте из подкаталога Ex50 предложено более изящное решение: вначале воспроизводится внутренняя поверхность сферы, а затем внешняя:

glEnable(GL_BLEND); // включаем смешение
glEnable(GL_CULL_FACE); // включаем отсечение сторон полигонов
glCullFace(GL_FRONT); // не воспроизводить лицевую поверхность сферы
draw_sphere(Angle); // вывести заднюю поверхность сферы
glCullFace(GL_BACK); // не воспроизводить заднюю поверхность сферы
draw_sphere(Angle); // вывести переднюю поверхность сферы
glDisable(GL_CULL_FACE); // отключить сортировку поверхностей
glDisable(GLJ3LEND); // отключить режим смешения

Замечание
Решение хорошее, но для простых задач можно все-таки воспроизводить только внешнюю сторону объекта, так будет быстрее

Теперь мы подошли к еще одной проблеме, связанной с полупрозрачностью. Сделаем конус в нашей системе также полупрозрачным (проект из подкаталога Ех51). Для этого альфа-компонент для материала, из которого изготовлен конус, задаем отличным от единицы, в данном случае равным 0.5, как и для сферы.

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

Учитывая то, что мы уже сталкивались с проблемой замкнутых полупрозрачных объектов, сортируем передние и задние поверхности и конуса, и сферы:

glEnable(GL_BLEND);
glEnable(GL_CULL_FACE);
// рисуем заднюю поверхность конуса и сферы
glCullFace(GL_FRONT);
draw_cone;
draw_sphere(Angle);
// рисуем переднюю поверхность конуса и сферы
glCullFace(GL_BACK);
draw_cone;
draw_sphere(Angle);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);

Казалось бы, этого достаточно, однако сфера при прохождении за конусом становится невидимой. Эта проблема также ставит многих в тупик, когда на сцене должны присутствовать несколько полупрозрачных объектов. Здесь уже нельзя ограничиться воспроизведением только задней или передней поверхности, иначе картинка получается или неестественной, или тусклой.

Рис. 4. 31. Если на сцене присутствует несколько полупрозрачных объектов требуются дополнительные ухищрения для достижения реализма в изображении

На рис. 4. 31 изображен снимок работы программы из подкаталога Ex51 оба объекта стали действительно полупрозрачны при любых положениях в пространстве.
Решение состоит в том, чтобы согласовать содержимое буфера смешения и буфера глубины, первым нужно воспроизводить наиболее удаленный объект

glEnable(GL_BLEND);
glEnable(GL_CULL_FACE);
If Angle < 180 then begin
// сфера за конусом, первой воспроизводим сферу
glCullFace(GL_FRONT); // вначале задние стороны
draw_sphere(Angle + 45. 0);
draw_cone;
glCullFace(GL_BACK); // затем передние
draw__sphere(Angle + 45. 0);
draw_cone();
end
else begin
// конус за сферой, первым воспроизводим конус
glCullFace(GL_FRONT); // вначале задние стороны
draw_cone();
draw_sphere(Angle + 45. 0);
glCullFace(GL_BACK); // затем передние
draw_cone();
draw_sphere(Angle + 45. 0);
end;
glDlsable(GL_CULL_FACE);
glDisable(GL_BLEND);

Рис. 4 32 демонстрирует работу еще одного красивого примера, проекта из подкаталога Ex53

Рис. 4. 32. Обязательно посмотрите, как работает этот красивый пример

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

const
density = 36;
Введен класс, соответствующий отдельному пятну:

type
TDrip = class public
// массивы цветов пятна
outer_color, nng_color, inner_color: Array [0.. 3] of GLfloat;
outer_radius, ring_radius: GLfloat;
procedure Draw;
procedure fill_points;
private
divisions: GLint; // деления, соответствуют плотности
points: Array [O.. density * 8 - 1] of GLfloat; // точки пятна
end;

Следующие константы задают максимальное количество одновременно присутствующих на экране пятен и максимальный радиус пятна, по достижении которого пятно тает:

const
max_drips = 20; max_ring_radius = 250.0;

Нам необходимо запоминать позицию центра каждого пятна, а также иметь массив, хранящий данные на каждое пятно:

var
dnp_position : Array [0..max_drips-l, 0. 1] of GLfloat;
first_drip, new_drip : GLint; // текущее количество пятен
drips : Array [0. .max_dnps - 1] of TDrip;

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

procedure TDrip.Draw; // метод - нарисовать отдельное пятно
var
1 : GLint;
begin
glBegin(GL__TRIANGLES) ;
For i := 0 to divisions-1 do begin
glColor4fv(@inner_color);
glVertex2f(0.0, 0.0); // треугольники, выходящие из центра пятна
glColor4fv(@ring_color);
glVertex2f(points[2*i] * ring_radius, points[2*i + 1] * ring_radius),
glVertex2f(points[2*((i+l) mod divisions)] * ring_radius,
points[(2*((i+l) mod divisions)) + 1] * ring radius);
end;
glEnd;
end;

При заполнении массива точек пятна берутся значения, равномерно лежащие внутри круга:

procedure TDrip.fill_pomts;
var
i : GLint;
theta : GLfloat;
delta : GLfloat;
begin
delta := 2.0 * PI / divisions;
theta := 0.0;
For i := 0 to divisions-1 do begin
points[2 * i] := cos(theta);
points[2 * i + 1] := sin(theta);
theta := theta + delta;
end;
end;

Если, например, синус умножить на какое-нибудь число, получатся не круги, а овалы
Пользовательская процедура create_drip вызывается при каждом щелчке кнопкой мыши, ей передаются координаты центра нового пятна и требуемый цвет:

procedure create_drip(x, у, г, g, b : GLfloat);
begin
drips [new_drip] := TDrip. Create/With drips[new_drip] do begin
divisions := density,
fill_points;
inner_color[0] := r;ring_color[0] := r;outer_color[0] := r;
inner_color [ 1 ] := g;nng_color [1] := g;outer_color [1] := g;
inner_color[2] : = b;ring_color[2] := b;outer_color[2] := b;
// альфа-компонент края пятна нулевой
inner_color[3] := 1.0;ring_color[3] := 1.0;outer_color[3] := 0.0;
ring_radius := 0.0;outer_radius := 0.0;
end;
drip_position[new_drip][0] := x;
drip_position[new_drip][1] := y;
// увеличиваем счетчик пятен
new_drip := (new_drip + 1) mod max_drips;
If (new_drip = first_drip)
then first_drip := (first_drip + 1) mod max_drips;
end;

При очередном воспроизведении кадра радиусы всех присутствующих на экране пятен увеличиваются, при достижении максимального радиуса пятно растворяется.
Анимация создается простым зацикливанием, но вряд ли этот пример будет работать где-то чересчур быстро, т к используется так называемый "альфа блэндинг" (blend - смешивать):

Procedure TfrmGL.WMPaint(var Msg: TWMPaint);
var
ps : TPaintStruct; rel_size : GLfloat; i : GLint;
begin
BeginPaint(Handle, ps);
i := first_drip;
glClear(GL_COLOR_BUFFER_BIT);
While i<>new_drip do begin
drips[i].ring_radius := drips[i].ring_radius + 1;
drips[i].outer_radius := drips[i].outer_radius + 1;
rel size := drips[i].ring_radius / max_ring_radius;
// корректируем альфа-компонент, края пятна полупрозрачные
drips[i]. nng_color [3] := 0;
drips[i].inner_color[3] := 5-5*rel_size*rel_size;
// смещаемся в центр пятна
glPushMatrix;
glTranslatef(drip_position[i][0], drip_position[i][1], 0.0);
drips[i].draw; // рисуем пятно
glPopMatrix;
// пятно достигло максимального размера
If (drips[i].ring_radius > max_ring_radius)
then first_drip := (first_drip + 1) mod max_drips;
i:=(i+l) mod max_drips;
end;
SwapBuffers(DC);
EndPaint(Handle, ps);
InvalidateRect(Handle, nil, False); // зацикливаем программу
end;

Если на вашем компьютере пример работает недостаточно быстро, можете увеличить шаг, с которым увеличиваются радиусы пятен.

Подробнее о пиксельных операциях
В этом разделе мы рассмотрим несколько полезных примеров, имеющих отношение не только к функциям OpenGL работы с пикселами, но и к некоторым другим темам.
Начнем с проекта из подкаталога Ех54, иллюстрирующего, как можно комбинировать различные проекции В примере используются ортографическая и перспективная проекции
На экране присутствуют прямоугольная площадка и система из двух объектов - сферы и цилиндра (рис. 4.33).

Рис. 4.33. В этом примере содержимое экрана запоминается в массиве

Фиолетовая сфера надвигается на наблюдателя и "растворяется" После того как сфера ушла из поля зрения, на сцене остается только фоновая картинка
Трюк заключается в следующем: при самом первом воспроизведении кадра рисуется только площадка, служащая фоном Содержимое экрана сразу же после этого запоминается в массиве, делается как бы слепок экрана:

If first then begin // сделать только один раз
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glCallList(walls); // нарисовать площадку фона
// делаем снимок с экрана
glReadPixels(О, О, 255, 255, GL_RGBA, GL_UNSIGNED_BYTE, @pixels);
first := FALSE; // устанавливаем флаг
Makelmage; // подготовка списка фона
end
else glCallList(zaplmage); // вызов списка фона glPushMatrix;
// рисуем фиолетовую сферу
glTranslatef (20.0, 5.0, 5.0 + dz);
glCallList (sphere);
glPopMatrix;

Если не использовать особых приемов, то либо на экране останутся следы от сферы, либо фоновая картинка не будет объемной.
Для простоты массив, хранящий снимок с экрана, взят постоянных размеров, под клиентскую область экрана 255x255:

Pixels : Array [0..254, 0..254, 0..3] of GLUbyte;

Из-за этого упрощения при изменении размеров окна картинка портится и даже возможно аварийное завершение работы приложения

Замечание
Можно либо запретить изменять размеры окна, либо менять размерность массива при их изменении.

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

procedure TfrmGL.Makelmage;
begin
glNewList(zapImage,GL_COMPILE);
glDisable(GL_LIGHTING);
glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER BIT);
glMatrixMode(GL_PROJECTION);
glPushMatrix; glLoadldentity;
glOrthof(0.0, ClientWidth, 0.0, ClientHeight, -5.0, 50.0);
glMatrixMode(GLJMODELVIEW);
glPushMatrix; glLoadldentity;
glRasterPos2i(0,0);
glPopMatrix;
glMatrixMode(GL_PROJECTION);
glPopMatrix;
glMatrixMode(GL_MODELVIEW);
glDisable(GL_DEPTH_TEST); // буфер глубины - только для чтения
// вывод на экран массива пикселов
glDrawPixels(ClientWidth, ClientHeight, GL RGBA, GL UNSIGNED BYTE,
@pixels);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEndList;
end;

Рис. 4.34. Часто разработчики нуждаются в нескольких экранах

Следующий пример не имеет отношения к функциям работы с пикселами, но подготавливает к следующим примерам и сам по себе является очень полезным. В проекте из подкаталога Ех55 в окне приложения присутствует несколько экранов, в каждом из которых одна и та же сцена видна из различных точек зрения (рис. 4.34).
Для разбиения окна на несколько экранов пользуемся знакомым нам приемом, основанным на использовании команд glviewport и glscissor. Напомню, что вторая из них нам необходима для того, чтобы при очистке буфера кадра не закрашивался весь экран.
Для сокращения кода видовые параметры задаются одинаковыми для всех трех экранов:

procedure TfrmGL.FormResize(Sender: TObject);
begin
glMatrixMode(GL_PROJECTION);
glLoadldentity;
gluPerspective (60.0, ClientWidth / ClientHeight, 5.0, 70.0);
glMatrixMode (GL_MODELVIEW);
glLoadldentity;
InvalidateRect(Handle, nil, False);
end;

Конечно же, для каждого экрана можно эти параметры задавать индивидуально, тогда соответствующие строки необходимо вставить перед заданием установок, например, перед очередным glViewport. Для каждого экрана меняем положение точки зрения командой gluLookAt:

glPushMatrix;
// первый экран - левая половина окна
glviewport(0,0,round(ClientWidth/2), ClientHeight);
glScissor(0,0,round(ClientWidth/2), ClientHeight); //вырезка
glClearColor(0. 55, 0. 9, 0. 4, 0. 0);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
gluLookAt(25. 0, 25. 0, 50. 0, 25. 0, 25. 0, 20. 0, 0. 0, 1. 0, 0. 0);
glTranslatef(25. 0, 25. 0, 10. 0);
glRotatef (Angle, 1. 0, 0. 0, 0. 0);
glCallList(Torus);
glPopMatrix;
// второй экран - правый верхний угол окна
// единица - для получения разделительной линии
glViewport(round(ClientWidth/2) + l, round(ClientHeight/2) +1,
round(ClientWidth/2), round(ClientHeight/2));
glScissor(round(ClientWidth/2) + l, round(ClientHeight/2) +1,
round(ClientWidth/2), round(ClientHeight/2));
glClearColor(0. 7, 0. 7, 0. 9, 0. 0);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
gluLookAt(25. 0, 50. 0, 50. 0, 25. 0, 25. 0, 20. 0, 0. 0, 1. 0, 0. 0);
glTranslatef(25. 0, 25. 0, 10. 0); glRotatef (Angle, 1. 0, 0. 0, 0. 0);
glCallList(Torus);
glPopMatrix;
// третий экран - левый нижний угол окна
glViewport(round(ClientWidth/2) +l, 0, round(ClientWidth/2),
round(ClientHeight/2));
glScissor(round(ClientWidth/2)+l, , round(ClientWidth/2), round(ClientHeight/2));
glClearColor(0. 0, 0. 6, 0. 7, 0. 0);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
gluLookAt(0. 0, 25. 0, 50. 0, 25. 0, 25. 0, 20. 0, 0. 0, 1. 0, 0. 0);
glTranslatef(25. 0, 25. 0, 10. 0);
glRotatef (Angle, 1. 0, 0. 0, 0. 0);
glCallList(Torus);
glPopMatrix;
glPopMatrix;

Стоит напомнить, что без следующей строки ничего работать не будет.

glEnable(GL_SCISSOR_TEST); // включаем режим вырезки

Два рассмотренных примера подготовили нас к тому, чтобы познакомиться со следующим проектом из подкаталога Ex56, где визуализируется содержимое буфера глубины (рис 4.35).

Рис. 4. 35. Содержимое буфера глубины доступно для визуализации

Окно приложения не изменяется в размерах, для хранения содержимого буфера глубины введен массив с размерностью, подогнанной под размеры окна:

Zbuf: Array [0.. WIN_WIDTH - 1, 0.. WIN HEIGHT - 1] of GLfloat;

Для хранения значения глубины на каждый пиксел экрана требуется одно вещественное число. Левая половина окна является обычной областью вывода, в ней рисуется вращающийся тор После рисования тора содержимое буфера глубины копируется в массив. Для второго экрана видовые параметры задаются так, чтобы удобно было установить позицию растра, затем массив zbuf копируется на экран:

glViewport(0, 0, round(ClientWidth/2), ClientHeight);
glScissor(0, 0, round(ClientWidth/2), ClientHeight);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
// для правого экрана задается перспективная проекция
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(60. 0, ClientWidth / ClientHeight, 5. 0, 70. 0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity;
glPushMatrix;
gluLookAt(25. 0, 25. 0, 50. 0, 25. 0, 25. 0, 20. 0, 0. 0, 1. 0, 0. 0);
glTranslatef(25. 0, 25. 0, 10. 0);
glRotatef (Angle, 1. 0, 0. 0, 0. 0);
glCallList(Torus);
// копируем в массив содержимое буфера глубины
glReadPixels(0, 0, WIN_WIDTH, WIN_HEIGHT, GL_DEPTH_COMPONENT, GL FLOAT,
Zbuf);
glPopMatrix;
// левый экран
glViewport(round(ClientWidth/2) +1, 0, round(ClientWidth/2), ClientHeight);
glScissor(round(ClientWidth/2) + 1, 0, round(ClientWidth/2), ClientHeight);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
glRasterPos2i (-1, -1) ; // позиция вывода в координатах экрана
// вывод содержимого массива
glDrawPixels(WIN_WIDTH, WIN_HEIGHT, GL_LUMINANCE, GL_FLOAT, @Zbuf);
glPopMatrix;

Содержимое массива (буфера глубины) выводится оттенками серого, пикселы, наиболее близко расположенные к наблюдателю, выглядят темнее, глубина максимальна для наиболее удаленных точек.
Если третий аргумент команды glDrawPixels изменить, например, на GL_GREEN, получим ту же самую картинку в оттенках зеленого. Комбинируя значения пятого аргумента команды glReadPixels и третьего параметра команды glDrawPixels, можно получать различные видеоэффекты (посмотрите мою пробу на эту тему в подкаталоге Ех57).
Возможно, вам потребуется, чтобы на экране присутствовало только одно трансформированное изображение. Двойная буферизация будет здесь как никогда кстати.
В проекте из подкаталога Ех57 демонстрируется, как это можно сделать (рис. 4.36).

Рис. 4.36. Объекты можно рисовать стеклянными

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

Буфер накопления
Этот буфер аналогичен буферу цвета, но при выводе в него образы накапливаются и накладываются друг на друга. Используется буфер для получения эффектов нерезкости и сглаживания. Посмотрим на примере проекта из подкаталога Ех59, как осуществляется работа с буфером накопления.

Рис. 4.37. Пример на использование буфера накопления

Пример очень простой: на экране нарисованы два разноцветных прямоугольника, частично перекрывающие друг друга, в области пересечения происходит смешение цветов (рис. 4.37). Первые две цифровые клавиши задают режим воспроизведения полигонов, линиями или сплошной заливкой. При инициализации работы приложения вызовом команды glclearAccum задается значение, которым заполняется буфер накопления. Воспроизведение кадра состоит в том, что прямоугольники рисуются в буфере кадра по отдельности, в буфере накопления изображения накапливаются. Ключевую роль здесь играет команда glAccum:

glClear (GL_COLOR_BUFFER_BIT) ;
glCallList(thingl); // красный прямоугольник
// буфер кадра загружается в буфер накопления с коэффициентом 0.5
glAccum (GL_LOAD, 0.5);
glClear(GL_COLOR_BUFFER_BIT); // экран очищается
glCallList(thing2); // зеленый прямоугольник
// наложение старого содержимого буфера накопления
//с содержимым буфера кадра
glAccum(GL_ACCUM, 0.5);
// содержимое буфера накопления выводится в буфер кадра
glAccum(GL_RETURN, 1.0);

Здесь требуются некоторые пояснения.
Если вторым аргументом команды glAccum задается GL_LOAD, старое содержимое буфера аккумуляции затирается, подменяется содержимым буфера кадра.
Первые три действия примера можно осуществлять и так, т. е. явным образом очищая буфер накопления.

// очищаются буфер кадра и буфер накопления
glClear(GL_COLOR_BUFFER_BIT or GL_ACCUM_BUFFER_BIT);
glCallList(thingl); // красный прямоугольник
// содержимое буфера кадра смешивается с содержимым буфера накопления
// (пустым)
glAccum(GL_ACCUM, 0.5);

Вторым параметром команды glAccum можно манипулировать для управления яркостью изображения, в данном примере полигоны помещаются в буфер накопления с коэффициентом 0 5, т е их яркость уменьшается вполовину. При итоговом вызове этой команды яркость можно увеличить, коэффициент может принимать значения больше единицы
Теперь перейдем к проекту из подкаталога Ех59, рисующему на экране несколько объектов из модуля GLUT (рис 4.38).

Рис. 4.38. Эта композиция станет тестовой для примеров на буфер накопления

Здесь нет особых приемов, работа с буфером накопления отсутствует, но эта программа послужит основой для следующих примеров
Вот в проекте из подкаталога Ех61 выводится та же самая картина, но нерезкой, размытой

Замечание
Возможно, в примере эффект мало заметен, но после того, как разберем программу, вы сможете его усилить.

Чтобы смазать изображение, в буфере накопления одна и та же сцена рисуется несколько раз, каждый раз с немного измененной точкой зрения. Например, чтобы изображение двоилось, надо нарисовать сцену два раза под двумя точками зрения Чем больше мы возьмем таких точек зрения, тем более ровным окажется размытость, но такие эффекты, конечно, затормаживают воспроизведение
Можно пожертвовать качеством изображения, но повысить скорость взять небольшое количество точек зрения, но увеличить расстояние между ними в пространстве. В примере используется модуль jitter, переложение на Delphi известного модуля jitter h Модуль содержит в виде констант набор массивов, хранящих координаты точек зрения, а точнее - смещений точки зрения, разбросанных вокруг нуля по нормальному (Гаусса) закону. Массивов всего семь, массив с именем j2 содержит данные для двух точек зрения, j66 содержит j66 таких точек. Тип элементов массивов следующий:

type
jitter_point = record
х, у : GLfloat;
end;

В нашем примере введена константа, задающая, сколько "кадров" будет смешиваться в буфере накопления:

const
ACSIZE = 8;

В примере установлена ортографическая проекция:

procedure TfrmGL.FormResize(Sender: TObject);
begin
glViewport(0, 0, ClientWidth, ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadldentity;
If ClientWidth <= ClientHeight
then glOrtho (-2.25, 2.25, -2 25*ClientHeight/ClientWidth,
2.25*ClientHeight/ClientWidth, -10.0, 10.0)
else glOrtho (-2.25*ClientWidth/ClientHeight, 2.25*ClientWidth/ClientHeight, -2.25, 2.25, -10.0, 10.0);
glMatrixMode(GLJMODELVIEW);
InvalidateRect(Handle, nil, False);
end;

Из параметров команды glOrtho видно, что расстояния между левой и правой, верхней и нижней плоскостями отсечения одинаковы и равны 4 5 Это число мы будем использовать в качестве базового при кодировании эффекта размытости, как масштаб для перевода в мировые координаты:

Procedure TfrmGL.DrawScene;
var
viewport : Array[0..3] of GLint;
jitter of GLint, 222
begin
glGetlntegerv (GL_VIEWPORT, viewport) ;
// viewport[2] = ClientWidth
// viewport[3] = ClientHeight
glClear(GL_ACCUM_BUFFER_BIT);
For Jitter := 0 to ACSIZE - 1 do begin
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
// Эта формула преобразовывает дробное перемещение пиксела
//в мировые координаты
glTranslatef (38[jitter].x*4.5/viewport[2],
38 [jitter] .yM.5/viewport[3] , 0.0) ;
displayObjects; // рисуем сцену
glPopMatrix;
glAccum(GL_ACCUM, 1.0/ACSIZE); // в буфер накопления
end;
glAccum (GL_RETURN, 1.0); // буфер накопления - в буфер кадра
SwapBuffers(DC); // вывод кадра на экран
end;

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

Замечание
В примере используется массив из восьми точек, если на вашем компьютере этот пример работает слишком медленно, можете уменьшить это число до трех, а базовое число 4 5 в формулах преобразования увеличить раза в три.

Если для вас важно качество изображения, но пауза перед воспроизведением кадра недопустима, можете отказаться от двойной буферизации, а команду SwapBuffers заменить на glFlush.
Думаю, теперь этот пример для вас понятен, и мы можем перейти к следующему, где буфер накопления используется для получения эффекта фокуса Проект из подкаталога Ех62 содержит программу, рисующую всё ту же композицию. Первоначально изображение ничем не отличается от картинки из предыдущего примера.
Для получения эффекта необходимо задать базовую точку в пространстве, находящуюся в фокусе, или расстояние от наблюдателя до этой точки Точка зрения немного переносится в пространстве, перспектива немного усиливается Чем сильнее усиливается коэффициент перспективы, тем менее узкая область пространства не теряет резкость Сцена воспроизводится в искаженной перспективе, и процесс повторяется несколько раз с разных точек зрения. Для облегчения кодирования в программе написаны две вспомогательные процедуры, основная из которых AccFrustum. Первые шесть аргументов процедуры идентичны аргументам команды glFrustum.
Аргументы pixdx и pixdy задают смещение в пикселах для нерезкости Оба устанавливаются в нуль при отсутствии эффекта размытости Параметры eyedx и eyedy задают глубину области, в пределах которой сцена не размывается, если оба они равны нулю, то на сцене не будет присутствовать область, находящаяся в фокусе. Аргумент focus задает расстояние от глаза наблюдателя до плоскости, находящейся в фокусе Этот параметр должен быть больше нуля (не равен).
Поскольку процедура использует команду переноса системы координат, до ее вызова видовые параметры должны быть заданы с учетом будущего переноса:

procedure AccFrustum(left, right, bottom, top: GLdouble;
anear, afar, pixdx, pixdy, eyedx, eyedy: GLdouble; focus: GLdouble);
var
xwsize, ywsize : GLdouble; // размеры окна
dx, dy : GLdouble;
// для хранения параметров области вывода
viewport : array[0. 3] of GLint;
begin
glGetlntegerv (GL_VTEWPORT, @viewport); // получаем размеры окна
xwsize := right - left; // дистанция в пространстве по горизонтали
ywsize := top - bottom; // дистанция в пространстве по вертикали
// приращения для искажения перспективы
dx := -(pixdx*xwsize/viewport[2] + eyedx*anear/focus);
dy := -(pixdy*ywsize/viewport[3] + eyedy*anear/focus);
glMatrixMode(GL_PROJECTION);
glLoadldentity();
// меняем перспективу на чуть искаженную
glFrustum (left + dx, right + dx, bottom + dy, top + dy, anear, afar) ;
glMatrixMode(GLJMODELVIEW); glLoadldentity();
// переносим центр сцены
glTranslatef {-eyedx, -eyedy, 0.0);
end,

В принципе, этой процедуры достаточно, но для тех, кто привык пользоваться командой gluPerspective для задания перспективы, написана процедура AccPerspective, первые четыре аргумента которой идентичны аргументам gluPerspective Смысл остальных параметров мы разобрали выше:

procedure AccPerspective(fovy, aspect, anear, afar, pixdx, pixdy,
eyedx, eyedy, focus: GLdouble);
var
fov2,left,right,bottom,top : GLdouble;
begin
// половина угла перспективы, переведенного в радианы
fov2 := "fovy*Pi) / 180.0) / 2.0;
// рассчитываем плоскости отсечения
top := anear / (cos(fov2) / sin(fov2));
bottom := -top;
right := top * aspect;
left := -right;
AccFrustum (left, right, bottom, top, anear, afar, pixdx, pixdy, eyedx, eyedy,
  focus);
end;

Эта процедура вызывается перед каждым воспроизведением системы объектов. Для получения эффекта фокуса можете переписать вызов, например, так:

accPerspective (50.0, viewport[2]/viewport[3],
1.0, 15.0, ;j8 [jitter] .x, j8[jitter].у, 0.33*j8[jitter] .x, 0.33*;)8 [jitter] .у, 4.0);

Последний аргумент можете варьировать для удаления плоскости фокуса от глаза наблюдателя. Следующие два примера отличаются от предыдущих только наполнением сцены. В проекте из подкаталога Ех62 пока отсутствуют какие-либо эффекты, просто нарисовано пять чайников из различного материала, каждый из которых располагается на различном расстоянии от глаза наблюдателя (рис. 4.39).

Рис. 4.39. Эта композиция станет тестовой для дополнительного примера на эффект фокуса

В проекте из подкаталога Ех64 нарисованы те же пять чайников, но в фокусе находится только второй из них, золотой, остальные объекты размыты.

Замечание
Для подготовки каждого кадра может потребоваться несколько секунд

Используются все те же самые процедуры AccFrustum и AccPerspective. Еще приведу несколько примеров на эту тему.
В проекте из подкаталога Ех65 мы не встретим ничего принципиально нового. Рисуется шестнадцать раз чайник с различных точек зрения, только не используется отдельный модуль, а массивы пикселных смещений описаны здесь же. Среди этих массивов вы можете найти дополнительные наборы данных, например, массив на 90 точек.
На рис. 4.40 приведен результат работы программы из подкаталога Ех66, также приводимой только для закрепления темы.

Рис. 4.40. Объекты сцены в примере рисуются нерезкими

Здесь эффект нерезкости используется в анимационном приложении, нажатием на клавишу 'А' можно включать/выключать этот эффект.

Туман
Туман является самым простым в использовании спецэффектом, предназначенным для передачи глубины пространства. Он позволяет имитировать атмосферные эффекты дымки и собственно тумана.
При его использовании объекты сцены перестают быть чересчур яркими и становятся более реалистичными, естественными для восприятия.
Посмотрим на готовом примере, как работать в OpenGL с этим эффектом.

Рис. 4.41. Пример на использование тумана

Рис. 4.41 демонстрирует работу проекта из подкаталога Ех67: пять чайников, располагающихся от наблюдателя на различном удалении, атмосфера наполнена дымом.
Цвет дыма в примере задается серым, что является обычным значением при использовании тумана:

fogColor : array[0..3] of GLfloat = (0.5, 0.5, 0.5, 1.0);

При инициализации задаем параметры дымки:

glEnable(GL_FOG); // включаем режим тумана
fogMode := GL_EXP; // переменная, хранящая режим тумана
glFogi(GL_FOG_MODE, fogMode); // задаем закон смешения тумана
glFogfv(GL_FOG_COLOR, fogColor); // цвет дымки
glFogf(GL_FOG_DENSITY, 0.35); // плотность тумана
glHint(GL_FOG_HINT, GL_DONT_CARE);// предпочтений к работе тумана нет

Здесь при инициализации закон "затухания" тумана задается наиболее подходящим для обычных задач значением - GL_EXP, помимо этой константы могут использоваться еще GL_LINEAR и GL_EXP2 (в файле справки приведены соответствующие формулы, по которым происходит потеря цвета).
Приложение имеет всплывающее меню, позволяющее по выбору пользователя задавать различные законы затухания тумана. Обратите внимание, что для линейного закона задаются расстояния до ближней и дальней плоскостей отсечения, в пределах которых туман затухает по линейному закону, более удаленные предметы растворяются в дыму:

procedure TfrmGL.SelectFog(mode: GLint); // выбор режимов тумана begin
case mode of 1 : begin
glFogf(GL_FOG_START, 1.0); // ближняя плоскость для линейного
// затухания
glFogf(GL_FOG_END, 5.0); // дальняя плоскость для линейного
// затухания
glFogi(GL_FOG_MODE, GL_LINEAR); // линейный закон
InvalidateRect(Handle, nil, False);
end;
2 : begin
glFogi(GL_FOG_MODE, GL_EXP2);
InvalidateRect(Handle, nil, False);
end;
3 : begin
glFogi(GL_FOG_MODE, GL_EXP); InvalidateRect(Handle, nil, False); end;
0 : close;
end;
end;

Приведу еще один несложный пример, проект из подкаталога Ех68, иллюстрирующий, как туман можно использовать для передачи глубины пространства (рис. 4.42).

Рис. 4.42. Обычно эффект дымки используется для передачи глубины пространства

Пример действительно простой: в легкой дымке рисуется правильный многогранник. В таких картинках наблюдатель обычно путается с определением того, какие ребра фигуры более удалены, при использовании же дымки никакой неопределенности не возникает. Обратите внимание, что в этом примере мы снова встречаемся с функцией glHint; пожелание к системе OpenGL состоит в том, чтобы туман эмулировался с наилучшим качеством:

Procedure myinit;
const
fogColor : Array [0..3] of GLFloat = (0.0, 0.0, 0.0, 1.0); // цвет тумана
begin
glEnable(GL_FOG); // включаем туман
glFogi(GL_FOG_MODE, GL_LINEAR); // линейный закон распространения
glHint (GL_FOG_HINT, GL_NICEST); // пожелания к передаче тумана
glFogf (GL_FOG_START, 3.0); // передняя плоскость тумана
glFogf (GL_FOG_END, 5.0); // задняя плоскость тумана
glFogfv (GL_FOG_COLOR, @fogColor); // задаем цвет тумана
glClearColor(0.0, 0.0, 0.0, 1.0);
glDepthFunc(GL_LESS); glEnable(GL_DEPTH_TEST);
glShadeModel(GL_FLAT);
end;

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

Рис. 4.43. Для простых задач тень можно рисовать самому, по многоугольникам

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

procedure TfrmGL.Shadow;
// подсчет координат точки тени для одной вершины
procedure OneShadow (х, у, z, h : GLfloat; var x1, y1 : GLfloat);
begin
x1 := x * LightPosition [2] / (LightPosition [2] - (z + h) ) ;
If LightPosition [0] < x
then begin If x1 > 0 then x1 := LightPosition [0] + x1 end
else begin If x1 > 0 then x1 := LightPosition [0] - x1 end;
y1 := у * LightPosition [2] / (LightPosition [2] - (z + h));
If LightPosition [1] < у
then begin If y1 > 0 then y1 := LightPosition [1] + y1 end
else begin If y1 > 0 then yl := LightPosition [1] - y1 end;
If x1 < 0 then x1 := 0 else
If x1 > SquareLength then x1 := SquareLength;
If y1 < 0 then y1 := 0 else
If y1 > SquareLength then y1 := SquareLength;
end;
var
x1, y1, x2, y2, хЗ, уЗ, х4, y4 : GLfloat;
wrkx1, wrky1, wrkx2, wrky2, wrkx3, wrky3, wrkx4, wrky4 : GLfloat;
begin
OneShadow (cubeX + cubeL, cubeY + cubeH, cubeZ, cubeW, x1, y1);
OneShadow (cubeX, cubeY + cubeH, cubeZ, cubeW, x2, y2);
OneShadow (cubeX, cubeY, cubeZ, cubeW, хЗ, уЗ);
OneShadow (cubeX + cubeL, cubeY, cubeZ, cubeW, x4, y4);
// пол на уровне -1 по оси Z, тень рисуется над полом, -0.99 по оси Z
If cubeZ + cubeW >= 0 then begin
glBegin (GL_QUADS); // тень от верхней грани объекта
glVertex3f (x1, y1, -0.99);
glVertex3f (x2, y2, -0.99);
glVertex3f (хЗ, уЗ, -0.99);
glVertexSf (x4, y4, -0.99);
glEnd;
end;
If cubeZ >= 0 then begin wrkx1 := x1;
wrky1 := y1;
wrkx2 := x2;
wrky2 := y2;
wrkx3 := хЗ;
wrky3 := уЗ;
wrkx4 := х4;
wrky4 := у4;
OneShadow (cubeX + cubeL, cubeY + cubeH, cubeZ, 0, x1, y1) ;
OneShadow (cubeX, cubeY + cubeH, cubeZ, 0, x2, y2) ;
OneShadow (cubeX, cubeY, cubeZ, 0, хЗ, уЗ) ;
OneShadow (cubeX + cubeL, cubeY, cubeZ, 0, x4, y4) ;
glBegin (GL_QUADS);
glVertex3f (x1, y1, -0.99) // нижняя грань
glVertexSf (x2, y2, -0.99)
glVertexSf (хЗ, уЗ, -0.99)
glVertex3f (x4, y4, -0.99)
glVertex3f (wrkx2, wrky2, -0.99); // боковые грани
glVertex3f (x2, y2, -0.99)
glVertexSf (хЗ, уЗ, -0.99)
glVertex3f (wrkx3, wrky3, -0.99);
glVertex3f (wrkx1, wrky1, -0.99);
glVertex3f (wrkx4, wrky4, -0.99);
glVertexSf (x4, y4, -0.99);
glVertex3f (x1, y1, -0.99);
glVertexSf (wrkx1, wrky1, -0.99);
glVertex3f (x1, y1, -0.99);
glVertex3f (x2, y2, -0.99);
glVertexSf (wrkx2, wrky2, -0.99);
glVertexSf (wrkx3, wrky3, -0.99);
glVertexSf (хЗ, уЗ, -0.99);
glVertexSf (x4, y4, -0.99);
glVertexSf (wrkx4, wrky4, -0.99);
glEnd;
end;
end;

Этот пример не универсальный, край пола я намеренно убрал за грани:, экрана, чтобы скрыть ошибку, возникающую при приближении тени к этому краю.
Перейдем к следующему примеру, проекту из подкаталога Ех70, результат работы которого представлен на рис. 4.44.

Рис. 4.44. Получение тени с помощью специальных средств библиотеки OpenGL

Сцена воспроизводится для каждого кадра два раза, над полом окрашенной, под полом - бесцветной.
Для получения сероватой тени от объектов сцены используется смешение цветов и буфер трафарета. Рассмотрим, как это делается.
Параметры, задаваемые для смешения цветов, традиционны:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Буфер трафарета заполняется нулями до рисования тени, а когда рисуется тень, значение в буфере трафарета увеличивается для каждого пиксела, где она присутствует:

glClearStencil(0);
glStencilOp(GL_INCR, GL_INCR, GL_INCR);
glStencilFunc(GL_EQUAL, 0, $FFFFFFF);

Тестирование буфера трафарета используется для воспроизведения только тех пикселов, где значение в буфере трафарета равно нулю; поскольку значение увеличивается, то соответствующие пикселы исключаются из дальнейшего использования.
В результате каждый пиксел тени используется только один раз, чтобы тень получалась однотонной.
При нажатии на клавишу 'С' можно отключать использование буфера трафарета, в этом случае некоторые участки тени становятся темнее из-за того, что при смешении цвета значение альфа-компонента удваивается при наложении объектов. Булевская переменная usestencil является флагом, задающим режим использования буфера трафарета.
Для проекции объектов на плоскость пола используется пользовательская Матрица проекции с нулем на диагонали, чтобы проецировать Y-координату в нуль:

mtх[0,0] := 1.0;
mtx[1,1] := 0;
totx[2,2] := 1.0;
totх[3,3] := 1.0;

Поскольку объекты будут рисоваться дважды, код для их воспроизведения вынесен в отдельную процедуру DrawScene
Теперь перейдем непосредственно к коду кадра:

// рисуем пол, источник света отключаем, иначе пол чересчур темный
glPushMatrix;
glDisable (GL_LIGHTO);
glDisable (GL_LIGHTING);
glColor3f(0.8, 0.8, 1) ;
glBegin(GL_QUADS);
glVertex3f(-0.5, -0.5, 0.5);
glVertex3f(0.5, -0.5, 0.5);
glVertexSf(0.5, -0.5, -0.5);
glVertex3f(-0.5, -0.5, -0.5);
glEnd;
glEnable (GL_LIGHTING); glEnable (GL_LIGHTO);
glPopMatrix;
// рисуем объекты над полом, в красном цвете
glPushMatrix;
glColor3f(1, 0, 0);
glRotatef(Angle, 1, 1, 1) ;
DrawScene;
glPopMatrix;
// рисуем тень
If useStencil then begin
// используется ли буфер трафарета
glClear(GL_STENCIL_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);
end;
// проецируем объекты на пол
glPushMatrix;
glColor4f(0, 0, 0, 0.3); // цвет объектов задаем черным
glTranslatef(0, -0.5, 0);
glMultMatrixf(Smtx); // для корректной проекции на плоскость пола
glRotatef(Angle, 1, 1, 1);
glDisable(GL_DEPTH_TEST); // отключаем буфер глубины
DrawScene; // рисуем фальшивую сцену под полом
glEnable(GL_DEPTH_TEST); // возвращаем обычный режим
glPopMatrix; glDisable(GL_STENCIL_TEST);

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

Рис. 4.45. Площадка с тенью перемещается в пространстве

Следующий пример является прямым потомком предыдущего в проекте из подкаталога Ех71 площадка с тенью вращается вокруг системы объектов, как будто источник света также перемещается в пространстве (рис 4.45).
Обратите внимание, что в отличие от предыдущего примера объекты сцены окрашены в разные цвета. Чтобы этого не произошло с тенями объектов и они остались бы серыми, процедуру воспроизведения сцены я снабдил параметром, задающим, включать ли источник света и надо ли использовать цвета:

procedure DrawScene (light : Boolean);
begin
glPushMatrix;
If light then begin
glEnable (GL_LIGHTING);
glEnable (GL_LIGHTO);
glColor3f(l, 0.3, 0.5);
end;
glutsolidTorus(0.1, 0.2, 16, 16);
If light then glColorSf(0.5, 0.8, 0.8);
glTranslatef(0.05, 0.08,-0.2);
glutSolidSphere(0.05, 16, 16);
glTranslatef(0.2, 0.2, 0.4);
glutsolidSphere(0.1, 16, 16);
glTranslatef(0.3, 0.3, -0.2);
If light then begin glDisable (GL_LIGHTO);
glDisable (GL_LIGHTING);
end;
glPopMatrix;
end;

При воспроизведении кадра нет никаких хитрых перемножений матриц, система координат ложной сцены перемещается вслед за площадкой и производится масштабирование:

procedure TfrmGL.WMPaint(var Msg: TWMPaint);
var
ps : TPaintStruct;
begin
BeginPaint(Handle, ps);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glPushMatrix;
glPushMatrix; // пол
glColor3f(0.8, 0.8, 1) ;
glRotatef(Angle, 0, 1, 0) ;
glBegin(GL_POLYGON);
glVertex3f(0.5, -0.5, 0.5);
glVertex3f(0.5, 0.5, 0.5);
glVertexSf(0.5, 0.5, -0.5);
glVertex3f(0.5, -0.5, -0.5);
glEnd;
glPopMatrix; glPushMatrix; // тень
glClear(GL_STENCIL_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);
glColor4f(0, 0, 0, 0.4);
glTranslatef(0.5*c, 0, 0.5*a); // перемещение для тени
glScaled(abs(a),1,abs(с)); // отобразить изменения в расстоянии
glDisable(GL_DEPTH_TEST);
glRotatef(AngleSystem, 1, 1, 1) ;
DrawScene (False);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glPopMatrix;
glRotatef(AngleSystem, 1, 1, 1); // собственно система объектов
DrawScene (True);
glPopMatrix; SwapBuffers(DC); EndPaint(Handle, ps);
Angle := Angle +5; // угол поворота площадки
If Angle >= 360.0 then Angle := 0.0;
AngleSystem := AngleSystem + 2.5; // угол поворота системы объектов
If AngleSystem >= 360.0 then AngleSystem := 0.0;
a := -sin(Angle * Pi/180);
b := 0;
с := cos(Angle * Pi/180);
InvalidateRect(Handle, nil, False); // зацикливаем программу
end;

Замечание
Обратите внимание, что в обоих примерах тень рисуется всегда, даже тогда, когда выходит за границы пола.

Переходим к проекту из подкаталога Ех72, очень интересному примеру, результат работы которого представлен на рис. 4.46.

Рис. 4.46. Использование буфера трафарета для получения узоров на плоскости

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

procedure TfrmGL.copyDepthToColor(whichColorBuffer : GLenum);
var
x, у : GLint; max, nun : GLfloat;
previousColorBuffer : GLint;
begin
// заполняем массив depthSave содержимым буфера глубины
glReadPixels(0, 0, winWidth, winHeight, GL_DEPTH_COMPONENT, GL_FLOAT,
@depthSave);
// инвертирование содержимого массива
depthSave max := 0;
nun := 1;
For у := 0 to winHeight - 1 do
For x := 0 to winWidth - 1 do begin
If (depthSave[winWidth * у + x] < nun)
then mm := depthSave[winWidth * у + x] ;
If (depthSave[winWidth * у + x] > max) and (depthSave[winWidth *
у + x] < 0.999)
then max := depthSave[winWidth * у + x] ;
end;
For у := 0 to winHeight - 1 do
For x := 0 to winWidth - 1 do If (depthSave[winWidth * у + x] <= max)
then depthSave[winWidth * у + x] := 1 - (depthSave[winWidth *
у + x] - mm) / (max - mm) else depthSave[winWidth * у + x] := 0;
// меняем проекцию, чтобы удобнее задавать позицию вывода растра
pushOrthoView(0, 1, 0, 1, 0, 1) ;
glRasterPos3f(0, 0, -0.5); // можно
glRasterPos2f(0, 0)
glDisable(GL_DEPTH_TEST); // вывод только в буфер кадра
glDisable(GL_STENCIL_TEST);
glColorMask(TRUE, TRUE, TRUE, TRUE);
// запоминаем текущий буфер вывода
glGetIntegerv(GL_DRAW_BUFFER, @previousColorBuffer);
glDrawBuffer(whichColorBuffer); // задаем требуемый буфер вывода
// выводим оттенками серого содержимое массива
glDrawPixels(winWidth, winHeight, GL_LUMINANCE, GL_FLOAT, @depthSave);
// восстанавливаем первоначальные установки glDrawBuffer(previousColorBuffer);
glEnable(GL_DEPTH_TEST);
popView; // восстанавливаем видовые параметры
end;

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

const // вспомогательный массив для передачи содержимого буфера трафарета
colors : Array [0..6, 0..2] of Byte = ( (255, 0, 0), // красный
(255, 218, 0), // желтый
(72, 255, 0), // желтовато-зеленый
(0, 255, 145), // голубоватый циан
(0, 145, 255), // цианово-синий
(72, 0, 255), // синий с пурпурным оттенком
(255, 0, 218) // красноватый пурпурный );
// процедура передачи буфера трафарета цветами
procedure TfrmGL.copyStencilToColor(whichColorBuffer : GLenum);
var
x, у : GLint;
previousColorBuffer : GLint;
stencilValue : GLint;
begin
// считываем в массиве stencilSave содержимое нужного буфера
glReadPixels(0,0,winWidth,winHeight,GL_STENCIL_INDEX,GLJJNSIGNED BYTE,
@ stencilSave);
// перевод значений в свою палитру
For у := 0 to winHeight - 1 do
For x := 0 to winWidth - 1 do begin
stencilValue := stencilSave (winWidth * у + x);
colorSavef[(winWidth * у + x)*3+0] := colors[stencilValue mod [7][0];
colorSave[(winWidth * у + x)*3+1] := colors[stencilValue mod 7][1];
colorSave[(winWidth * у + x)*3+2] := colors[stencilValue mod 7][2];
end;
// меняем матрицу проекций для задания позиции вывода растра
pushOrthoView(0, 1, 0, 1, 0, 1);
glRasterPos3f(0, 0, -0.5);
glDisable(GL_DEPTH_TEST); // вывод только в буфер кадра
glDisable (GL_STENCIL__TEST) ;
glColorMask(TRUE, TROE, TRUE, TRUE);
glGetIntegerv(GL_DRAW_BUFFER, glpreviousColorBuffer);
glDrawBuffer(whichColorBuffer); // передаем содержимое буфера трафарета в цвете
glDrawPixels(winWidth, winHeight, GL_RGB, GL_UNSIGNED_BYTE,ScolorSave) ;
// восстанавливаем первоначальные установки
glDrawBuffer(previousColorBuffer);
glEnable(GL_DEPTH_TEST);
popView;
end;

Базовая площадка, земля аэродрома, рисуется всегда, но менее удаленные объекты
  должны загораживать ее:

procedure TfrmGL.setupBasePolygonState(maxDecal : GLint);
begin
glEnable(GL_DEPTH_TEST);
If useStencil then begin
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, maxDecal + 1, $ff); // рисуется всегда
glStencilOp(GL_KEEP, GL_REPLACE, GL_ZERO);
end
end;

Все остальные объекты сцены в своих пикселах увеличивают значение буфера трафарета, загораживая более удаленные объекты

procedure TfrmGL.setupDecalState(decalNum : GLint);
begin
If useStencil then begin
glDisable(GL_DEPTH_TEST);
glDepthMask(FALSE);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_GREATER, decalNum, $ff) ;
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
end
end;

Воспроизведение объектов сцены не содержит для нас ничего нового, поэтому рассмотрим подробно только итоговый код воспроизведения кадра:

procedure TfrmGL.WMPaint(var Msg: TWMPamt) ;
var
ps : TPaintStruct; label // метка для передачи управления
doneWithFrame;
begin
BeginPaint(Handle, ps);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
// буфер трафарета очищается только при включенном режиме
If dataChoice = STENCIL
then glClear(GL_STENCIL_BUFFER_BIT);
glPushMatrix;
glScalef(0.5, 0.5, 0.5);
If stage = 1 then goto doneWithFrame; // выбран этап 1
setupLight;
setupNormalDrawingState; // без записи в буфер трафарета
glPushMatrix;
glTranslatef (0, 1, 4);
glRotatef(135, 0, 1, 0);
drawAirplane; // рисуем самолет
glPopMatrix;
If stage = 2 then goto doneWithFrame; // выбран этап 2
setupBasePolygonState(3) ; // 3 - для трех картинок на земле
drawGround; // рисуем землю
If stage = 3 then goto doneWithFrame; // выбран этап 3
setupDecalState(1); // наклейка 1 - асфальт взлетной полосы
drawAsphalt; // рисуем асфальт
If stage = 4 then goto doneWithFrame; // выбран этап 4
setupDecalState(2); // наклейка 2 - желтые полосы на асфальте
drawStripes; // рисуем полосы
If stage = 5 then goto doneWithFrame; // выбран этап 5
setupDecalState(3); // наклейка 3 - тень от самолета
glDisable(GL_LIGHTING); // тень рисуется без источника света
glEnable(GL_BLEND); // обязательно включить смешение цвета
glPushMatrix;
glColor4f(0, 0, 0, 0.5); // цвет тени - черный, альфа < 1.0
glTranslatef(О, О, 4); // сдвигаем систему координат под землю
glRotatef(135, О, 1, 0); // подгоняем систему координат для тени
glScalef(1, О, 1);
drawAirplane; // рисуем копию самолета - тень
glPopMatrix;
glDisable(GL_BLEND);
glEnable(GL_LIGHTING); (label) doneWithFrame:
setupNormalDrawingState; // восстановить нормальные установки
glPopMatrix;
// если выбран вывод буферов, все предыдущее затирается
If dataChoice = STENCIL then copyStencilToColor(GL_BACK);
If dataChoice = DEPTH then copyDepthToColor(GL_BACK);
SwapBuffers(DC);
EndPaint(Handle, ps);
end;

Надеюсь, этот пример помог вам лучше уяснить многие вопросы, связанные с буферами. Эффект отражения плоским зеркалом реализуется в OpenGL способом, похожим на то, что мы использовали для тени - объекты рисуются дважды, над и под поверхностью отражения. Для такого эффекта достаточно одного смешения цветов, буфер трафарета используется здесь для того, чтобы фальшивая система не выглядывала за пределами зеркальной поверхности.
На рис. 4. 47 показан один из моментов работы следующего примера, располагающегося в подкаталоге Ex73.

Рис. 4. 47. Простейший пример на получение эффекта зеркального отражения

Код воспроизведения следующий:

procedure TfrmGL. WMPaint(var Msg: TWMPaint);
var
ps: TPaintStruct; Begin
BeginPaint(Handle, ps);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BOFFER_BIT);
glLoadIdentity;
glTranslatef(0, -0. 5, -4);
// здесь пол рисуется только в буфер трафарета
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, $FFFF); // пол рисовать всегда
glColorMask(FALSE, FALSE, FALSE, FALSE);
glDisable(GL_DEPTH_TEST);
DrawFloor; // собственно пол
// восстанавливаем нормальные установки
glColorMask(TRUE, TRUE, TRUE, TRUE);
glEnable(GL_DEPTH_TEST);
// отражение рисуется только там, где значение в буфере
// трафарета равно единице
glStencilFunc(GL_EQUAL, 1, $FFFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
// рисуем отраженную сцену
glPushMatrix;
glScalef(l, -1, 1); // переворачиваем по оси Y
DrawObjects;
glPopMatrix;
// рисуем настоящий пол, полупрозрачным, чтобы можно было увидеть
// отраженные объекты
glDepthMask(FALSE);
DrawFloor;
glDepthMask(TRUE);
// для воспроизведения подлинной системы отключаем буфер трафарета,
// иначе она также будет обрезаться областью пола
glDisable(GL_STENCIL_TEST);
DrawObjects;
glFinish;
SwapBuffers(DC);
EndPaint(Handle, ps);
Angle: = (Angle + 2) mod 360; // для поворота в следующем кадре
InvalidateRect(Handle, nil, False);
end;

Все вроде просто, но если вы внимательно посмотрите на пример, то обнаружите мою небольшую хитрость: окно приложения нельзя изменять в размерах.
Сделал я это специально, иначе при сужении окна по вертикали, начинает выглядывать фальшивая система объектов за границей пола.
Ответ на вопрос "а почему?" состоит в том, что третий аргумент команды glstencilOp при подготовке вывода фальшивой системы имеет не совсем
подходящее значение. Этот аргумент должен быть GL__ZERO или GL_INCR, но при таких значениях на поверхности отраженных объектов появляется непрошенный узор.
Мы уже встречали эту проблему и знаем, что она связана с наложением альфа-компонентов для замкнутых фигур, знаем также, что она легко решается сортировкой поверхностей.
Привожу окончательный вариант соответствующего фрагмента кода; можете убедиться, что после внесения изменений все работает совершенно корректно:

glStencilFunc(GL_EQUAL, I, $FFFF) ;
glStencilOp(GLJCEEP, GLJCEEP, GL_INCR);// здесь изменен третий аргумент
glPushMatrix;
glScalefd, -1, 1);
glCullFace (GL_FRONT);//сортировка поверхностей, внутренняя и внешняя
glEnable (GL_CULL_FACE); // поверхности воспроизводятся отдельно
DrawObjects;
glCullFace (GL_BACK);
DrawObjects;
glDisable (GL_CULL_FACE);
glPopMatrix;

Познакомимся теперь, как можно создавать эффект многократного отражения. Рис. 4.48 демонстрирует работу следующего примера, проекта из подкаталога Ех74: на стенах комнаты висят два зеркала, одно напротив другого, объекты сцены многократно отражаются в этих зеркалах.

Рис. 4.48. Пример на создание многократного отражения

Клавишами управления курсором можно регулировать, как глубоко распространяется это отражение, нажатием на клавишу 'Н' можно менять точку зрения, при взгляде сверху демонстрируются ложные дубликаты объектов.
Главной в этом примере является процедура draw_scene, используемая рекурсивно для многократного воспроизведения объектов сцены:

const
stencilmask : longint = $FFFFFFFF;
procedure TfrmGL.draw_scene(passes:GLint; cullFace:GLenum;
stencilVal:GLuint; mirror: GLint);
var
newCullFace : GLenum;
passesPerMirror, passesPerMirrorRem, i : GLint;
curMirror, drawMirrors : GLUint;
begin
// один проход, чтобы сделать реальную сцену
passes := passes - 1;
// рисуем только в определенных пикселах
glStencilFunc(GL_EQUAL, stencilVal, stencilmask);
// рисуем вещи, которые могут закрыть первые зеркала
draw_sphere;
draw_cone;
// определяем номер отражений зеркала
If mirror о -1 then begin
passesPerMirror := round(passes / (nMirrors - 1));
passesPerMirrorRem := passes mod (nMirrors - 1) ;
If passes > nMirrors - 1
then drawMirrors := nMirrors - 1
else drawMirrors := passes;
end
else begin
// mirror == -1 означает, что это - начальная сцена (не было
// никаких зеркал)
passesPerMirror := roundfpasses / nMirrors);
passesPerMirrorRem := passes mod nMirrors;
If passes > nMirrors
then drawMirrors := nMirrors
else drawMirrors := passes;
end;
i := 0;
While drawMirrors > 0 do begin
curMirror := i mod nMirrors;
If curMirror <> mirror then begin
drawMirrors := drawMirrors - 1;
// рисуем зеркало только в буфере шаблона
glColorMask(False, False, False, False);
glDepthMask(False);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
draw_mirror(mirrors[curMirror]);
glColorMask(True, True, True, True);
glDepthMask(True) ;
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // рисуем отраженную сцену
newCullFace := reflect_through_mirror(mirrors[curMirror], cull-Face) ;
If passesPerMirrorRemoO then begin
// рекурсивное обращение самой к себе
draw_scene(passesPerMirror + 1, newCullFace, stencilVal + 1, curMirror);
passesPerMirrorRem := passesPerMirrorRem - 1;
end
else draw_scene(passesPerMirror, newCullFace, stencilVal + 1,
curMirror);
// возвращаем видовые параметры
undo_reflect_through_mirror(mirrors[curMirror], cullFace);
// обратная сторона в нашей величине шаблона
glStencilFunc(GL_EQUAL, stencilVal, stencilmask);
end;
me (i) ;
end;
draw_room;
end;

В этой главе нас ждет еще один пример с использованием эффекта зеркального отражения, так что мы не прощаемся с этой темой окончательно.
Закончу раздел примером, в котором хотя и эмулируется зеркало, однако непосредственно эффекта отражения не создается
Откройте проект из подкаталога Ех75. После запуска откомпилированного модуля на экране появляется следующая картина по кругу, в центре которого находится глаз наблюдателя, вращаются четыре цветных кубика В верхней части экрана располагается прямоугольник, в котором как в зеркальце заднего вида отражается кубик, находящийся за спиной наблюдателя (рис. 4.49).

Рис. 4.49. В зеркальце можно увидеть, что происходит за спиной наблюдателя

Для зеркальца на экране создается вырезка, точка зрения наблюдателя разворачивается на 180 градусов, и сцена воспроизводится второй раз:

// обычная точка зрения
glViewport(0, 0, ClientWidth, ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadldentity;
gluPerspective(45.0, ClientWidth / ClientHeight, 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadldentity;
gluLookAt(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
// воспроизводим систему из четырех кубиков
glPushMatrix;
glRotatef(Angle, 0.0, 1.0, 0.0);
glCallList(l);
glPopMatrix;
// точка зрения для зеркальца
glDisable(GL_LIGHTING); // чтобы линии не стали серыми
glViewport(ClientWidth shr 2, ClientHeight-(ClientHeight shr 2),
ClientWidth shr 1, ClientHeight shr 3) ;
glEnable(GL_SCISSOR_TEST); // вырезка для зеркальца
glScissor(ClientWidth shr 2, ClientHeight-(ClientHeight shr 2),
ClientWidth shr 1, ClientHeight shr 3);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadldentity; glMatrixMode(GL_PROJECTION);
glLoadldentity;
glOrtho(-1.001, 1.001, -1.001, 1.001, -1.001, 1.001); // белая рамочка вокруг
  зеркальца
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_LINE_LOOP);
glVertex3i(-1, 1, 1);
glVertex3i( 1, 1, 1);
glVertex3i( 1,-1, 1);
glVertexSi (-1,-1, 1);
glEnd;
// вид внутри зеркальца glLoadldentity;
a := (ClientWidth shr 1) / (ClientHeight shr 3);
b := (ClientHeight shr 3)/ (ClientWidth shr 1);
gluPerspective(45.0*b, a, 0.1, 100.0);
// разворачиваемся на 180 градусов
glMatrixMode(GL_MODELVIEW);
glLoadldentity;
9luLookAt( 0.0, 0.0,-1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glEnable(GL_LIGHTING);
glScalef(-1.0, 1.0, 1.0); // важно - эмулируем зеркало
//Разворачиваем нормали кубиков, так эффектнее
glFrontFace(GL_CW);
glPushMatrix;
glRotatef(Angle, 0.0, 1.0, 0.0);
glCallList(l);
glPopMatrix;
glFrontFace(GL_CCW);
glDisable(GL_SCISSOR_TEST);

Шаблон многоугольников
В этом разделе мы подготовимся к легендарной теме использования текстуры в OpenGL.
Шаблон многоугольников является простым способом нанесения узоров на многоугольники, его можно использовать в качестве текстуры для плоских задач.
Сейчас последует не очень выразительный и зрелищный пример, но он во многом полезный. В проекте из подкаталога Ех76 рисуется плоская деталь, с которой мы начинали наше изучение примитивов OpenGL. Тогда нам пришлось изрядно потрудиться, чтобы нарисовать невыпуклый многоугольник.
Теперь все стало несколько проще: я ввел массив, хранящий координаты десяти вершин, лежащих на контуре фигуры:

detal : Array [0..9] of TVector =
((-0.23678, 0.35118, 0.0),
(-0.23678, 0.7764, 0.0),
(-0.37966, 0.7764, 0.0),
(-0.55, 0.60606, 0.0),
(-0.55, -0.4, 0.0),
(0.45, -0.4, 0.0),
(0.45, 0.60606, 0.0),
(0.27966, 0.7764, 0.0),
(0.13678, 0.7764, 0.0),
(0.13678, 0.35118, 0.0) );

Используются tess-объекты и просто перечисляются вершины контура:

gluTessBeginPolygon (tobj, nil);
gluTessBeginContour(tobj);
For i := 0 to 9 do
gluTessVertex(tobj, [Sdetalfi], (Metal [i]);
gluTessEndContour(tobj);
gluTessEndPolygon(tobj);

Вышесказанное пока не относится к теме раздела, а только позволяет нам вспомнить некоторые пройденные темы.
Теперь по поводу собственно узора, в данном случае штриховки. Для нее я взял массив из 16 одинаковых строчек:

const
р75 : Array [0..127] of GLUbyte =
( $аа, $аа, $аа, $аа, $ff, $ff, $ff, $ff,
...

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

glEnable(GL_POLYGON_STIPPLE);
glPolygonStipple(@p75);

Теперь деталь покрывается равномерным узором. В программе случайно меняется текущий цвет, вся штриховка монохромная. Она может быть и разноцветной; если для каждой вершины многоугольника цвет задавать индивидуально, то штриховка покроется цветными переливами.
Рис. 4.50 иллюстрирует работу следующего, совсем простого примера (подкаталог Ех77).

Рис. 4. 50. Пример на использование различных штриховок

Три прямоугольника верхней линии штрихуются с различными шаблонами, крайние прямоугольники штриховкой не покрываются. Для всех прямоугольников верхней строки текущий цвет задан белым.
Нижние прямоугольники рисуются все без штриховки, переход оттенков обеспечивается тем, что текущий цвет для каждого из прямоугольников задается со все увеличивающейся интенсивностью.
При использовании штриховки многоугольников в пространстве появляется особый эффект, рассмотрим его на следующих нескольких примерах.
В проекте из подкаталога Ex78 на экране рисуется вращающийся додекаэдр Программу отличает то, что при ее инициализации включается режим штриховки многоугольников. Штриховка первоначально задается точечной, и ничего необычного поначалу не заметно. Нажатием клавиши пробела можно поменять шаблон; при использовании второго, в виде насекомых, становится видно, что штриховка не проецируется на поверхность объекта, а заполняет плоскостным узором область, ограниченную объектом (рис 4 51).

Рис. 4. 51. Штриховка многоугольников используется чаще всего для плоскостных построений

Этим свойством можно пользоваться для придания эффекта некоторой призрачности. Проект из подкаталога Ex79 содержит программу, иллюстрирующую, как реализовать такой эффект. На сцене присутствуют знакомые нам по некоторым предыдущим примерам сфера и конус, но сфера здесь выглядит эфемерно (рис 4 52).

Рис. 4. 52. Штриховкой можно пользоваться для создания призрачных объектов

Поскольку в OpenGL из многоугольников строятся все объемные фигуры вообще, режим штриховки распространяется и на сферу
Шаблон штриховки в программе заполняется случайными числами:

procedure create_stipple_pattern(var pat: TPattern; opacity: GLfloat);
var
x, у: GLint;
Begin
For у: = 0 to 31 do Begin pat[y]: = 0;
For x: = 0 to 31 do
If (random > 0. 6) // чем меньше это число, тем плотнее штриховка
then pat[y]: = pat[y] xor (1 shl x);
end;
end;

При воспроизведении кадра режим штриховки включается только для сферы

glEnable(GL_POLYGON_STIPPLE);
draw_sphere(Angle);
glDisable(GL_POLYGON_STIPPLE);

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

create_stipple_pattern(spherePattern, 0.5) ;
glPolygonStipple(spherePattern);

Замечание
Старайтесь выносить подобные вычислительные операции за пределы собственно воспроизведения кадра

Если попытаться и второй объект сцены, конус, сделать таким же эфемерным, то сфера при прохождении за ним становится невидимой - конус закрывает ее, так как шаблоны штриховки у них одинаковы.
Посмотрите проект из подкаталога Ех80: здесь такого не происходит, поскольку для каждого объекта сцены шаблон задается индивидуально.

glEnable(GL_POLYGON_STIPPLE); // включить режим штриховки
glPolygonStipple(@conePattern); // задаем шаблон для конуса
draw_cone; // рисуем штрихованный конус
glPolygonStipple(spherePattern); // задаем шаблон для сферы
draw_sphere(Angle); // рисуем штрихованную сферу
glDisable(GL_POLYGON_STIPPLE); // выключим режим

Текстура
Текстура подобна обоям, наклеиваемым на поверхность. Тема этого раздела является самой интересной, но она столь обширна, что рассмотреть ее досконально нам не удастся, этому можно посвятить целиком отдельную книгу.
Думаю, что наилучшим образом вы разберетесь с данной темой только тогда, когда самостоятельно попробуете решить какую-то задачу, я же приведу набор готовых решений только для самых распространенных задач, возникающих при использовании текстуры в OpenGL.
Текстура бывает одномерной и двумерной. Одномерная текстура может использоваться только для нанесения узора в виде полосок, двумерная позволяет наносить прямоугольные образы.
Начнем изучение темы с одномерной текстуры. Подкаталог Ех81 содержит следующий пример: на объекты нескольких типов накладываются чередующиеся красные и синие полоски (рис. 4.53).

Рис. 4.53. Пример использования одномерной текстуры

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

var
TexWidth : GLint =16; // ширина текстуры
GenSOn : Boolean = True; // генерировать ли координату s
// параметром процедуры является требуемая ширина текстуры,
// число должно быть степенью двойки
procedure MakeTeximage (TexImageWidth : GLint);
const // параметры текстуры
TexParams : Array [0..3] of GLfloat = (0.0, 0.0, 1.0, 0.0);
var
Texlmage : Array [0..128 * 3] of GLUbyte; // массив текстуры
3 : GLint; // вспомогательная переменная begin
3 := 0; // чередование красной и синей полосок
While ] < TexImageWidth * 3 - 1 do begin // заполнение массива образа
Texlmage [j] := 255; // красный
Texlmage [3 + 1] := 0; // зеленый
Texlmage [3 + 2] := 0; // синий
Texlmage [3 + 3] := 0; // красный
Texlmage [j + 4] := 0; // зеленый
Texlmage [: + 6] := 255; // синий
lnс (j, 6) ;
end;
// эти команды должны вызываться обязательно
glTexParameter (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameter (GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL~NEAREST) ,'
// собственно создание текстуры
glTexImagelD (GL_TEXTURE_1D, 0, 3, TexImageWidth, 0, GL_RGB,
GL_UNSIGNED_BYTE, STexImage); // генерировать ли координату s
If GenSOn
then glEnable (GL_TEXTURE_GEN_S'
else glDisable (GL_TEXTURE_GEN_S);
// уточняем параметры для координаты s, чтобы текстура не выглядела,
// как штриховка многоугольников
glTexGeni (GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGenfv (GL_S, GL_OBJECT_PLANE, @TexParams); // для поворота полосок
glEnable (GL_TEXTURE_1D); // включаем текстуру
end;

Здесь использован почти минимальный набор действий, которые необходимо выполнить, чтобы пользоваться текстурой; единственное, что можно удалить в этом коде, это строки, связанные с координатой s, но тогда пропадет возможность разворачивать полоски текстуры. Массив образа содержит значения RGB, задающие узор полосок (я задал значения для равных по ширине полосок красного и синего цвета) Команда glTexParameter позволяет задавать параметры текстуры Минимум, что надо сделать - задать значения для фильтров, как и написано в комментарии

Замечание
Если говорить точно, то первую строку можно удалить, но вторая строка должна присутствовать обязательно, даже и со значением, принятым по умолчанию.

Фильтры задают вид текстуры, когда площадь пиксела, на которую она накладывается, в одном случае больше, а в другом меньше элемента текстуры Эти значения я задал равными GL_NEAREST, что соответствует неточному расчету текстуры Другое возможное значение - GL_LINEAR, расчет будет точным, но за счет скорости. Если вы установите такое значение третьего параметра команды glTexParameter, то на границах полосок цвет будет плавно размываться.
Команда glTexImagelD вызывается каждый раз, когда надо задавать текстуру Для одномерной текстуры нужно использовать именно эту команду Ее первый параметр указывает на размерность, значение второго параметра, числа уровней детализации текстуры, никогда не меняйте и задавайте равным нулю Третий параметр задается равным трем или четырем для сохранения цвета, остальные значения приведут к потере цвета полосок. Дальше идет ширина текстуры; как видно из примера, она не обязательно должна совпадать с размером массива Пятый параметр, ширину границы, тоже рекомендую пока не трогать Шестой параметр задает формат данных, смысл остальных параметров понятен.
В общем, из всех параметров команды я рекомендую менять только ширину текстуры, ну и еще задавать имя массива
С текстурой связан набор координат, чаще всего вам потребуется работать с координатами s и t.
Группа команд glTexGen предназначена для установки функции, используемой для генерации координат текстуры Если этого не делать, то значение, задаваемое по умолчанию, приведет к тому, что текстура будет вести себя подобно штриховке, т. е не будет приклеена к поверхности В примере я взял значения аргументов команды, чаще всего используемые на практике, другие значения разберем позднее.
Для того чтобы полоски текстуры правильно разворачивались на сфере, перечисленных действий достаточно, но для цилиндра необходимо скорректировать карту расположения текстуры на объекте, в файле справки детально разбирается смысл всех параметров, связанных с координатами s и t.
Перед описанием списков вызывается команда, связанная с наложением текстуры на quadnc-объекты gluQuadricTexture (Quadric, TRUE);
Эта команда задает, генерировать ли координаты текстуры для таких объектов Если вы удалите эту строку, то при отключенной генерации координаты s текстуры quadnc-объекты (сфера и цилиндр) не будут покрыты текстурой.
Сейчас я вам советую дополнить набор объектов диском из библиотеки glu, а также многогранниками библиотеки glut и посмотреть, как текстура ложится на их поверхность
Координаты текстуры необходимо задавать для ее правильного отображения на поверхность. Введите в программу список для простого квадрата. Чтобы не было путаницы с координатами квадрата и текстуры, координаты вершин зададим отличными от 1. Для каждой вершины квадрата определяем ассоциированные вершины текстуры:

glBegin (GL_QOADS);
glTexCoord2d (0.0, 0.0); // s и t = 0
glVertex2f (-0.5, -0.5); // левый нижний угол квадрата
glTexCoord2d (1.0, 0.0); // s = 1, t = 0
glVertex2f (0.5, -0.5); // правый нижний
glTexCoord2d (1.0, 1.0); // s и t = 1
glVertex2f (0.5, 0.5); // правый верхний
glTexCoord2d (0.0, 1.0); // s = 0, t = 1
glVertex2f (-0.5, 0.5); // левый верхний
glEnd;

To есть для левого нижнего угла квадрата значения s и t нулевые, для правой нижней вершины значение t равно единице и т. д.
Теперь при отключенной генерации координаты s полоски правильно ложатся на квадрат. При включенном режиме значения всех элементов массива параметров должны быть нулевыми, за исключением первого. Для того чтобы перевернуть полоски, надо задавать ненулевым не первый, а второй элемент массива, если же оба их задать равными единице, полоски лягут по диагонали.
Теперь перейдем к двумерной текстуре. В проекте из подкаталога Ех82 поверхность многоугольников покрыта шашками (рис. 4.55).

Рис. 4.55. В примере используется двумерная текстура

Замечание
Для двумерной текстуры размеры массива образа должны быть степенью двойки, например, массив размером 256x512 подходит, а 255x255 - нет.

Помимо массива для основной текстуры вводится вспомогательный массив для подобраза, необходимый для работы команды glTexSubimage2D:

const
checklmageWidth = 64; // ширина текстуры шахматной доски
checklmageHeight = 64; // высота текстуры шахматной доски
sublmageWidth =16; // размеры вспомогательного массива
sublmageHeight = 16;
var
checklmage : Array [0..checklmageHeight - 1, 0..checklmageWidth - 1,
0..3] of GLubyte;
sublmage : Array [0..sublmageHeight - 1, 0..sublmageWidth - 1, 0..3] of GLubyte;

После того как массив заполнен, устанавливается двумерная текстура, параметры
  задаются только самые необходимые:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTex!mage2D(GL_TEXTURE_2D, 0, GL_RGBA, checklmageWidth,
checklmageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, @checklmage);

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

glEnable(GL_TEXTURE_2D); // включить режим
//не учитывать цвет примитивов
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
glBegin(GL_QUADS); // два независимых четырехугольника
glTexCoord2f(0.0, 0.0);
glVertexSf(-2.О, -1.0, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(-2.0, 1.0, 0.0);
glTexCoord2f(1.0, 1.0);
glVertexSf(0.0, 1.0, 0.0);
glTexCoord2f(1.0, 0.0);
glVertexSf(0.0, -1.0, 0.0);
glTexCoord2f(0.0, 0.0);
glVertexSf(1.0, -1.0, 0.0);
glTexCoord2f(0.0, 1.0);
glVertexSf(1.0, 1.0, 0.0);
glTexCoord2f(1.0, 1.0);
glVertexSf(2.41421, 1.0, -1.41421);
glTexCoord2f(1.0, 0.0);
glVertexSf(2.41421, -1.0, -1.41421);
glEnd;
9lDisable(GL_TEXTURE_2D); // режим отключается, немного экономится время

Режим GL_DECAL включается для того, чтобы цвета текстуры и поверхности Не смешивались. Можете задать текущим произвольный цвет - примитивы. не окрасятся, а если отключить этот режим, то шашки станут окрашенными. Здесь этот режим включается для того, чтобы не появлялись искажения при наложении текстуры на правую площадку. Пример иллюстрирует также работу команды glTexSubimage2D, позволяющей подменять часть образа текстуры. В примере при нажатии клавиши 'S' на первоначальной текстуре появляется квадратик с мелкой красной шашечкой, а после нажатия на 'R' текстура восстанавливается:

if Key = Ord ('S') then Begin // "Подобраз"
glTexSubImage2D(GL_TEXTURE_2D, 0,
12, 44, // x, у в координатах текстуры
subImageWidth, subImageHeight, GL_RGBA, GL_UNSIGNED_BYTE, @subImage);
InvalidateRect(Handle, nil, False);
end;
If Key = Ord ('R') then Begin // восстановление
// заново переустановить текстуру
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, checkImageWidth, checkImageHeight, 0,
  GL_RGBA, GL_UNSIGNED_BYTE, @checkImage);
InvalidateRect(Handle, nil, False);
end;

Команда glTexSubimage2D не представлена ни в файле справки, ни в модуле opengl. pas, ее прототип я описал в программе.
Рис. 4. 56 иллюстрирует работу следующего примера, располагающегося в подкаталоге Ex83 и подсказывающего, как можно наложить блики от источника света на поверхность, покрытую текстурой.

Рис. 4. 56. Для наложения бликов требуются дополнительные манипуляции

Если не применять особых ухищрений, то блик в такой ситуации не появляется, поскольку OpenGL применяет текстуру после прорисовки отражающей составляющей источника света. Для смешения бликов и текстуры необходимо выполнить двухшаговый алгоритм: нарисовать поверхность с текстурой без отражения источника света, включить смешение и перерисовать поверхность с матовым белым материалом и только отражающей составляющей источника света.
Пункты всплывающего меню позволяют посмотреть по отдельности действие каждого из этих этапов.
Следующий пример (проект из подкаталога Ex84) очень важен, несмотря на кажущуюся простоватость - не пропустите eго. На экране располагаются два объекта, каждый из них имеет свою текстуру (рис. 4. 57).

Рис. 4. 57. Если в кадре используется несколько текстур, эффективнее всего использовать или дисплейные списки, или связывание текстур

Начинающие часто спрашивают, как организовать работу с несколькими текстурами. Этот пример показывает два возможных способа эффективной организации такой работы.
Первый способ состоит в использовании дисплейных списков, здесь они оказываются как нигде кстати. На этом простом примере эффективность такого подхода пока мало заметна, но нас ждет еще один пример, где она проявится в полной мере.
Дисплейные списки при компиляции запоминают только команды OpenGL, поэтому при вызове списка все промежуточные действия по подготовке образа текстуры не будут затормаживать работу приложения Использование списков существенно облегчает работу также с точки зрения кодирования: нет необходимости держать массивы под каждый образ и переключаться между ними по ходу работы приложения.
В примере описываются два списка, каждый из которых содержит код по подготовке образов. Перед тем как нарисовать примитив, вызывается нужный список. Сами массивы образов могут уже использоваться для других нужд или вовсе не существовать (не существуют они и для нашей программы, а образы текстуры хранятся в памяти, занятой OpenGL).
Этот пример иллюстрирует и другой способ, основанный на использовании недокументированных команд.
Команда glBmdTexture позволяет связывать текстуры, создавать и вызывать именованные последовательности команд подобные дисплейным спискам, но предназначенные для идентификации только текстур. С ней связаны команда glGenTextures, генерирующая имена текстур, и команда glDeleteTextures, освобождающая память от текстурных списков

Замечание
Прототипы всех этих команд необходимо задавать самостоятельно

По выбору можно использовать любой из этих двух способов, выбор управляется переменной HaveTexObj.
В программе массив техОbj, состоящий из двух целых, хранит имена дисплейных или текстурных списков. Значения идентификаторов генерируются системой OpenGL:

if HaveTexObj // используются текстурные списки
then glGenTextures( 2, @TexObj) // генерировать два имени
else begin // используются дисплейные списки
TexObj[0] := glGenLists(2); // генерировать имена дисплейных
// списков
TexObj[1] := ТехОЬj[0]+1;
end;

При подготовке текстуры создается одна из двух разновидностей списков, для команды glBmdTexture концом описания списка является начало описания следующего:

if HaveTexObj // использовать текстурные списки
then glBmdTexture ( GL_TEXTURE_2D, TexObj [0]) // описание
else glNewList( TexObjf[0], GL_COMPILE); // начало для дисплейного
// списка
// красным на белом
For i:=0 to height-1 do
For j:=0 to width - 1 do begin p := i*width+];
if (texl[ (height-i-1) *width+;j]) <>0 then begin
tex[p] [0] := 255;
tex[p][1] := 0;
tex[p] [2] := 0;
end
else begin
tex[p][0] := 255;
tex[p][1] := 255;
tex[p][2] := 255;
end
end;
// собственно создание текстуры
glTexImage2D( GL_TEXTURE_2D, 0, 3, width, height, 0,
GL_RGB, GLJJNSIGNED_BYTE, @tex);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
If not HaveTexObj then glEndList;

Перед рисованием примитивов вызывается нужный список:

glPushMatrix;
glTranslatef( -1.0, 0.0, 0.0);
glRotatef( Angle, 0.0, 0.0, 1.0);
if HaveTexObj // какой из типов списков вызывать
then glBmdTexture ( GL_TEXTURE_2D, ТехОbj[0])
else glCallList( ТехОbj[0]);//здесь хранятся только параметры
glBegin( GL_POLYGON) ;
glTexCoord2f( 0.0, 0.0);
glVertex2f( -1.0, -1.0);
glTexCoord2f( 1.0, 0.0);
glVertex2f( 1.0, -1.0);
glTexCoord2f( 1.0, 1.0);
glVertex2f( 1.0, 1.0);
glTexCoord2f( 0.0, 1.0);
glVertex2f( -1.0, 1.0);
glEnd;
glPopMatrix;

В конце работы, как это принято в среде культурных программистов, память освобождается:

if HaveTexObj
then glDeleteTextures( 2, STexObj) // для текстурных списков
else glDeleteLists (TexObj[0], 2);

Возможно, вы захотите использовать для своих проектов традиционную для Демонстрационных программ текстуру в виде кирпичной кладки, тогда можете взять в качестве шаблона проект из подкаталога Ех85, где найдете все необходимые "стройматериалы"
(рис. 4 58).

Рис. 4.58. Такая текстура часто используется в демонстрационных проектах

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

Замечание
Поскольку я установил ограничение для примеров этой книги, содержащееся в том, что все они должны компилироваться в Delphi третьей версии, то буду
использовать bmp-файлы В старших версиях среды вы легко можете использовать другие форматы, что будет экономнее по отношению к дисковому пространству.Рис. 4.59 показывает один из моментов работы примера из подкаталога Ех86, в котором образ текстуры загружается из bmp-файла.

Рис. 4.59. Текстура загружается из файла

Файл картинки я позаимствовал из дистрибутива C++ Builder 4.0. Если мы заранее знаем размер файла, то можем использовать объект класса TBitmap для загрузки текстуры, а размеры массива образа задавать под размеры растра:

procedure TfrmGL.BmpTexture;
var
Bitmap: TBitmap;
Bits: Array [0..63, 0..63, 0..2] of GLubyte; // массив образа, 64x64
i, j: Integer; begin
Bitmap := TBitmap.Create;
Bitmap.LoadFromFile('gold.bmp1); // загрузка текстуры из файла
{---заполнение битового массива---}
For i := 0 to 63 do
For ] := 0 to 63 do begin
bits [i, j, 0] := GetRValue(Bitmap.Canvas.Pixels [1,3]);
bits [i, ;j, 1] := GetGValue(Bitmap.Canvas.Pixels[1,3]);
bits [i, j, 2] := GetBValue(Ritmap.Canvas.Pixels[1,3]);
end;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL NEAREST);
glTex!mage2D(GL_TEXTURE_2D, 0, GL_RGBA,
64, 64, // здесь задается размер текстуры
О, GL_RGB, GL_UNSIGNED_BYTE, @Bits); // чтобы цвет объекта не влиял на текстуру
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
glEnable (GL__TEXTURE_2D) ;
Bitmap.Free;
end;

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

Замечание
Для простоты в примерах я часто включаю режим использования текстуры при инициализации приложения. С точки зрения оптимизации надо включать и выключать ее для каждого кадра.

Следующий пример, проект из подкаталога Ех87, содержит подсказку, как быть в случае, если мы заранее не знаем размер растра или хотим использовать универсальный подход к этому вопросу. К тому же этот проект помогает тем читателям, которые приняли мое предложение о том, чтобы знакомиться с программированием, основанным только на использовании функций API.

Замечание
Для экономии места на дискете в оставшихся примерах я буду использовать ограниченный набор растров, хотя для многих примеров можно подобрать и более подходящие картинки.Этот и некоторые следующие примеры будут крутиться вокруг астрономической темы (рис. 4.60).

Рис. 4.60. Теперь мы можем гордиться своими астрономическими моделями

Файл растра с картой Земли я взял из DirectX SDK фирмы Microsoft Функция чтения растра основана на коде для методов класса TBitmap:

// функция возвращает размеры образа и указатель на массив образа
function ReadBitmap(const FileName : String;
var sWidth, tHeight: GLsizei): pointer; const
szh = SizeOf(TBitmapFileHeader); // размеры заголовка растра
szi = SizeOf(TBitmapInfoHeader);
type
TRGB = record
R, G, В : GLbyte;
end;
TWrap = Array [0..0] of TRGB;
var
BmpFile : File;
bfh : TBitraapFileHeader; // заголовок файла
bmi : TBitmapInfoHeader; x, size: GLint;
temp: GLbyte; // для перестановки красного и синего
begin
AssignFile (BmpFile, FileName);
Reset (BmpFile, 1);
Size := FileSize (BmpFile) - szh - szi;// заголовки в растр не включать
Blockread(BmpFile, bfh, szh); // считываем заголовки
BlockRead (BmpFile, bmi, szi);
If Bfh.bfType <> $4042 then begin // формат не подходит
MessageBox(Window, 'Invalid Bitmap', 'Error', MB OK);
Result .= nil;
Exit;
end;
sWidth := bmi.biWidth; // из заголовка узнаем размеры собственно растра
tHeight := bmi.biHeight;
GetMem (Result, Size); // выделяем память для массива образа
BlockRead(BmpFile, Result74, Size); // считываем собственно растр
For x := 0 to sWidth*tHeight-l do // переставить синий и красный
With TWrap(resultA)[x] do begin temp := R;
R := B;
В := temp;
end;
end;

В программе есть несколько упрощений, скорректируйте их, если хотите получить профессиональный код.
Я не стал загромождать текст и использовать защищенный режим (tryexcept), но вы должны обязательно его использовать для обработки возможных исключений. Кроме того, вы должны помнить о том, что описанная функция не может считывать монохромные растры Ниже я приведу более универсальный код, но для примеров, основанных на низкоуровневом подходе, я бы посоветовал вам посмотреть функцию чтения монохромных растров из последней главы этой книги. Чтобы применить функцию ReadBitmap, используем временно создаваемый указатель:

wrkPointer : Pointer;
sWidth, tHeight : GLsizei;
// указатель будет создан функцией
wrkPointer := ReadBitmap('..\earth.bmp', sWidth, tHeight,;
glTex!mage2D(GL_TEXTURE_2D, 0, 3, sWidth, tHeight, 0,
GL_RGB, GL_UNSIGNED_BYTE, wrkPointer);
Freemem(wrkPointer); // удаляем указатель

В следующем примере мы познакомимся сразу с несколькими важными вещами Во-первых, в нем используется немного другой подход к считыванию данных растра, а во-вторых, мы еще раз обсудим, как использовать несколько текстур в кадре
В проекте из подкаталога Ех88 вокруг планеты вращается спутник, каждый из объектов имеет свою текстуру (рис 4.61)

Рис. 4.61. Предлагаю вам дорисовать звезды

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

procedure TfrmGL.Preparelmage(bmap: string);
// тип для динамического массива, код подходит для всех версий Delphi
type
PPixelArray = ^TPixelArray;
TPixelArray = array [0..0] of Byte;
var
Bitmap : TBitmap;
Data : PPixelArray; // образ текстуры, размер заранее не оговариваем
BMInfo : TBitmapInfo; // заголовок файла
ImageSize : Integer;// вспомогательные переменные
Temp : Byte; // для перестановки цветов
MemDC : HOC; // вспомогательный идентификатор
begin
Bitmap := TBitmap.Create;
Bitmap.LoadFromFile (bmap); // считываем образ из файла
with BMinfо.bmiHeader do begin
FillChar (BMInfo, SizeOf(BMInfo), 0); // считываем заголовок
biSize := sizeof (TBitmapInfoHeader);
biBitCount := 24;
biWidth := Bitmap.Width;
biHeight := Bitmap.Height;
ImageSize := biWidth * biHeight;
biPlanes := 1;
biCompression := BI_RGB;
MemDC := CreateCompatibleDC (0);
GetMem (Data, ImageSize * 3); // создаем динамический массив
try
GetDIBits (MemDC, Bitmap.Handle, 0, biHeight, Data, // считываем
BMInfo, DIB_RGB_COLORS); // в DIB-формат растр
// битового массива
For I := 0 to ImageSize - 1 do begin // переставляем цвета
Temp := Data [I * 3]; Data [I * 3] := Data [1*3+2];
Data [1*3+2] := Temp,
end;
glTexImage2d(GLJTEXTURE_2D, 0, 3, biWidth, // создаем образ
biHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, Data);
finally
FreeMem (Data); // освобождаем память DeleteDC (MemDC);
Bitmap.Free;
end;
end;
end;

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

Quadric := gluNewQuadric;
gluQuadricTexture (Quadric, TRUE);
// обычные параметры текстуры
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN__FILTER, GL_NEAREST),
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// включать режим текстуры для оптимизации надо было бы в кадре
glEnable(GL_TEXTURE_2D);
// список для Земли
glNewList (Earth, GL_COMPILE);
Preparelmage ('..\earth.bmp'); // подготавливаем образ
gluSphere (Quadric, 1 0, 24, 24); glEndList; // список для Луны
glNewList (Moon, GL_COMPILE);
Preparelmage ('..\moon.bmp'); // образ другой
glPushMatrix; // будет перемещение
glTranslatef (1.3, 1.3, 0.3);
gluSphere (Quadric, 0.2, 24, 24);
glPopMatrix; // возвращаемся на место
glEndList;
// после описания списков quadnc-объекты больше не нужны
gluDeleteQuadric (Quadric);

Это очень важно, поэтому хочу еще раз повторить дисплейные списки в таких случаях являются крайне эффективным решением, иначе бы нам приходилось подготавливать и переключать образы текстуры многократно, для каждого кадра.
При описании списка мы обращаемся к процедуре чтения файла Preparelmage Здесь мог бы быть и более обширный код, но в дисплейный список компилируется только одна единственная строка, строка с подготовкой текстуры При вызове списка все промежуточные строки не вызываются, файл не считывается и, в принципе, вы можете ею даже удалить, т. к. он больше не используется.
По сценарию Земля вращается вокруг своей оси, а Луна вращается в противоположную Земле сторону:

glPushMatrix;
glRotatef (-10, 0.0, 1.0, 0.0);
glRotatef (Angle, 0.0, 0.0, 1.0); // поворот Земли вокруг своей оси
glCallList(Earth); glPopMatrix;
// для Луны необходимо добавить вращение вокруг своей оси
glPushMatrix;
glRotatef (-Angle, 0.0, 0.0, 1.0); // угол вращения противоположный
glCallList(Moon); glPopMatrix;

Переходим к следующему примеру, проекту из подкаталога Ex89, в котором показывается работа с полупрозрачной текстурой
(рис. 4.62).

Рис. 4.62. Только континенты непрозрачны

Для использования альфа-компонента потребовался вспомогательный динамический массив, в который последовательно перебрасываются данные из основного. Согласно замыслу точки с преобладанием синего цвета должны быть полупрозрачными, значение альфа для них задается маленьким, для всех остальных точек это значение равно 1 (вы можете варьировать все параметры для получения своих собственных эффектов):

GetMem (Data, ImageSize * 3); // обычный массив образа
GetMem (DataA, ImageSize * 4); // вспомогательный + альфа-компонент
// для точек try
GetDIBits (MemDC, Bitmap.Handle, 0, biHeight, Data, BMInfo, DIB RGB COLORS);
// переставляем синий и красный цвета
For I := 0 to ImageSize - 1 do begin
Temp := Data [I * 3];
Data [I * 3] := Data [I * 3 + 2];
Data [I * 3 + 2] := Temp;
end;
// точки с преобладанием синего делаем полупрозрачными
For I := 0 to ImageSize - 1 do Begin
DataA [I * 4] := Data [I * 3]; // перебрасываем данные
DataA [I * 4 + 1] := Data [I * 3 + 1];
DataA [I * 4 + 2] := Data [I * 3 + 2];
If (Data [I * 3 + 2] > 50) and // чтобы участки белого цвета
(Data [I * 3 + 1] < 200) and // не стали полупрозрачными
(Data [I * 3] < 200)
then DataA [I * 4 + 3] := 27 // степень прозрачности синего
else DataA [I * 4 + 3] := 255; // сплошным
end;
// !!! - при подготовке текстуры добавился альфа-компонент
glTexImage2d(GL_TEXTURE_2D, 0, 3, biWidth,
biHeight, 0,
GL_RGBA, // здесь надо было скорректировать
GL_UNSIGNED_BYTE, DataA);

При описании списка не забываем сортировать многоугольники, иначе появится ненужный узор:

glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glNewList (Earth, GL_COMPILE);
PrepareImage ('..\earth.bmp');
// оптимальнее включать режимы только при необходимости
glEnable (GL_BLEND);
glEnable(GL_CULL_FACE); // сортировка многоугольников
glCullFace(GL_FRONT);
gluSphere (Quadric, 1.0, 24, 24);
glCullFace(GL_BACK);
gluSphere (Quadric, 1.0, 24, 24);
glDisable(GL_CULL_FACE); // отключаем режимы
glDisable (GL_BLEND);
glEndList;

Думаю, здесь все понятно, и мы можем перейти к следующему примеру, чтобы научиться накладывать несколько текстур, что реализуется в проекте из подкаталога Ex90 (рис. 4.63).

Рис. 4.63. Теперь мы можем накладывать несколько текстур одновременно

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

glNewList (Earth, GL_COMPILE); // наружная сфера планеты
PrepareImage ('..\earth.bmp');
glEnable(GL_CULL_FACE); // сортируем многоугольники
glCullFace(GL_FRONT);
gluSphere (Quadric, 1.0, 24, 24);
glCullFace(GL_BACK);
gluSphere (Quadric, 1.0, 24, 24);
glDisable(GL_CULL_FACE);
glEndList;
// сфера со второй текстурой, располагается внутри первой
glNewList (Inside, GL_COMPILE);
PrepareImage ('..\grid.bmp');
glEnable(GL_CULL_FACE); // сортировка многоугольников
glCullFace(GL_FRONT);
gluSphere (Quadric, 0.99, 24, 24); // радиус чуть меньше, чем у Земли
glCullFace(GL_BACK);
gluSphere (Quadric, 0.99, 24, 24);
glDisable(GL_CULL_FACE);
glEndList;

Теперь важно воспроизводить объекты в правильном порядке (мы разбирали это в разделе, посвященном смешиванию цветов), иначе не получится наложения текстур:

glPushMatrix;
glRotatef (-10, 0.0, 1.0, 0.0);
glRotatef (Angle, 0.0, 0.0, 1.0);
glEnable (GL_BLEND); // так оптимальнее
glCallList(Inside); // вначале - внутреннюю сферу
glCallList(Earth); // потом - наружную
glDisable (GL_BLEND);
glPopMatrix;

Основные моменты, связанные с использованием текстуры, мы с вами изучили, теперь уточним еще некоторые вопросы
На рис. 4 64 представлена работа следующего примера, проекта из подкаталога Ex91, где наша знакомая модель неузнаваемо преобразилась Файл растра для этого примера я взял из DirectX SDK фирмы Microsoft:

Рис. 4.64. Текстуру можно использовать для эмуляции зеркального отражения

Секрет примера состоит не в удачном подборе образа текстуры, при любом другом картинка будет выглядеть так же превосходно. Главное в нем - это режим генерации координат текстуры, который задается приближенным к искажениям на поверхности сферы:

glTexGeni (GL_S, GL_TEXTURE_GEN__MODE, GL_SPHERE_MAP); // подобно сфере
glTexGeni (GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glEnable (GL_TEXTURE_GEN_S); // включается генерация обеих координат
glEnable (GL_TEXTURE_GEN_T);

Советую вам неспешно разобраться с этим примером, попробуйте поочередно отключать генерацию то одной, то другой координаты и посмотреть получающийся результат.Такой же эффект эмуляции зеркального отражения, но на поверхности цилиндра, демонстрируется в проекте из подкаталога Ex92 (рис 4.65).

Рис. 4.65. Металлические детали покрывайте текстурой для повышения зрелищности

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

// текстуру накладывать медленно, но точно
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// смешивать накладывающиеся цвета
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
// карта координат подобна сфере
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glEnable(GL_TEXTURE_2D);
// иначе будет нарисована цилиндрическая Земля
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);

Обратите внимание, что в этом примере перемещается не объект, а точка зрения
  наблюдателя:

glPushMatrix;
gluLookAt(lO*cos(spin), 5*cos(spin)*sin(spin), 15*sin(spin),
0.0, 0.0, 0.0, 1.0, 0.0, 0.0); glCallList(CyList);
glPopMatrix;

Не забывайте об этом примере, когда будете рисовать модели, где присутствуют цилиндрические детали, сделанные из металла.
Поверхности, покрытые текстурой, вполне пригодны для создания специальных эффектов. В проекте из подкаталога Ex93 на такой поверхности видно отражение объектов, располагающихся над ней (рис. 4.66). Само создание эффекта традиционно и заключается в том, что объекты сцены рисуются дважды, а для того чтобы скрыть от наблюдателя эту хитрость, используется буфер трафарета:

Рис. 4.66. Эффект зеркального отражения можно распространять и на поверхности, покрытые текстурой

glPushMatrix;
gluLookAt(eyex, eyey, eyez, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// рисуем стол в плоскости отражения
glEnable(GL_STENCIL_TEST); // буфер трафарета заполняем 1 там, где стол
glStencilFunc(GL_ALWAYS, 1, 1); // рисовать всегда, шаблон задаем = 1
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
// стол не рисуется в буфере кадра
glColorMask(FALSE, FALSE, FALSE, FALSE);
glCallList(table); // рисуем стол
glColorMask(TRUE, TRUE, TRUE, TRUE);
// точка отражения
If eyey > 0.0 then Begin
glPushMatrix;
glStencilFunc(GL_EQUAL, 1, 1); // рисуем при 1, только там, где стол
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glScalef(1.0, -1.0, 1.0);
draw_objects; // объекты сцены
glPopMatrix; end;
glDisable(GL_STENCIL_TEST);
glEnable(GL_BLEND); // на стол накладывается отражение
glCallList(table);
glDisable(GL_BLEND); // смотрим сверху
glPushMatrix;
draw_objects; // объекты рисуются обычным образом
glPopMatrix;
glPopMatrix;

Настала пора узнать, как в OpenGL можно использовать текстуру в качестве фона,
  и здесь нам поможет пример из подкаталога Ех94 (рис. 4.67).

Рис. 4.67. Текстуру можно использовать и в качестве фона

На заднем плане сцены рисуем квадрат, покрытый текстурой:

glMatrixMode(GL_PROJECTION);
glPushMatrix; // запоминаем нормальное значение матрицы проекций
glLoadldentity; // подгоняем так, чтобы квадрат занял
glOrtho(-50.0,50.О,-50. О, 50. О, 200.О, 300.0); // нужное положение
glMatrixMode(GL_MODELVIEW);
glDepthMask(FALSE); // без записи в буфер глубины
glEnable(GL_TEXTURE_2D); // текстура включается только для фона
glBegin(GL_QUADS); // квадрат, покрытый текстурой
glNormal3f(0.0,0.0,1.0);
glTexCoord2f(0.0,0.0) ;
glVertex3f(0.0,0.0,0.0);
glTexCoord2f(1.0,0.0);
glVertex3f(100.0,0.0,0.0);
glTexCoord2f(1.0,1.0);
glVertex3f(100.0,100.0,0.0);
glTexCoord2f(0.0,1.0);
glVertex3f(0.0,100.0,0.0);
glEnd;
glDisable(GL_TEXTURE_2D);
glDepthMask(TRUE); // возвращаем использование буфера глубины
glMatrixMode(GL_PROJECTION); // важно восстановить
glPopMatrix; // нормальное состояние в матрице проекций
glMatrixMode(GL_MODELVIEW);
glPushMatrix; // рисуем объекты сцены
glTranslatef(50.0, 50.0, 150.0);
glRotatef(Angle, 1.0, 1.0, 0.0);
glRotatef(Angle / (random (1) + I), 0.0, 0.0, 1.0);
glutSolidlcosahedron;
glPopMatrix;

Для этого добавьте следующие строки в процедуру инициализации:

glTexGeni (GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni (GLJT, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

Туда же переместите строку с включением режима использования текстуры, а генерацию
  координат текстуры надо включать только при рисовании объекта:

glEnable (GL_TEXTURE_GEN_S);
glEnable (GL_TEXTURE_GEN_T);
glutSolidTeapot (1.0);
glDisable (GL_TEXTURE_GEN_S);
glDisable (GL_TEXTURE_GEN_T);

В последнем примере главы я попытаюсь ответить сразу на несколько вопросов, часто возникающих у начинающих.
В проекте из подкаталога Ех95 рисуется волнообразная поверхность, на которой искаженно выводится текстура (рис. 4.69).

Рис. 4.69. Искажения образа текстуры позволяют добиться разнообразных эффектов

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

glEvalMesh2(GL_FILL, 0, 20, 0, 20); // вывод поверхности

В обработчике таймера пересчитываются координаты опорных точек поверхности, а также меняются координаты текстуры (для искажения образа):

A := А + 0.5; // увеличиваем управляющую переменную
init_surface; // пересчитываем координаты опорных точек
// ! - зарядить вычислитель новыми данными
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 17, 0, 1, 51, 17, @ctrlpoints);
// двигаем точки координат текстуры
texpts [O][0][0] :=texpts [O][0][0] -step;
texpts [0][0][1] :=texpts [0][0][1] -step;
// ограничиваем искажения некоторыми пределами
If (texpts [O][0][0] <-4.0) or (texpts [O][0][0] > 1.0)
then step := - step;
// принять во внимание измененные значения координат
glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2, @texpts);

При инициализации должна присутствовать строка с включением режима использования текстуры для поверхностей:

glEnable(GL_MAP2_TEXTURE_COORD_2);

Использование искажения координат текстуры открывает перед нами столь широкие возможности, что единственным ограничением является только наша фантазия.
Я бы мог привести еще массу примеров на использование текстуры в OpenGL, однако чувствую потребность остановиться, чтобы дать вам возможность самим попробовать свои силы.
Нет, все-таки не удержусь и приведу еще один (последний) пример, проект из подкаталога Ex96, иллюстрирующий, как подготовить текстуру на основе образа, считанного прямо с экрана, чтобы получить эффект увеличения (рис. 4.70).

Рис. 4.70. В качестве образа текстуры можно взять часть экрана

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

const eqn : Array [0..3] of GLdouble = (0.0, -1.0, 0.0, 0.0);
glNewList (Zoom, GL_COMPILE);
glClipPlane (GL_CLIP_PLANEO, @eqn);
glEnable (GL_CLIP_PLANEO);
glScalef (1.0, 0.15, 1.0); // уменьшаем кривизну полусферы
glEnable(GL_TEXTURE_2D);
gluSphere (Quadric, 0.5, 24, 24);
glDisable(GL_TEXTURE_2D); glDisable (GL_CLIP__PLANEO) ;
glEndList;

После воспроизведения основного объекта, планеты, считываем в массив часть экрана, затем используем этот массив в качестве образа для текстуры, накладываемой на полусферу:

glReadPixels (posX, posY, 128, 128, GL_RGB, GL_UNSIGNED_BYTE, @Pixels);
glPushMatrix;
glTranslatef (AddX, -5.0, AddZ);
glTex!mage2d(GL_TEXTURE_2D, 0, 3, 128, 128,
0, GL_RGB, GL_UNSIGNED_BYTE, @Pixels);
glCallList(Zoom) ;
glPopMatrix;

На этом примере главу 4 действительно можно считать законченной.


OpenGL в Delphi - ГЛАВА 5 Пример CAD-системы: визуализация работы робота

ГЛАВА 5 Пример CAD-системы: визуализация работы робота

Постановка задачи
Структура программы
Модули приложения
Обмен данными с DLL
Дополнительные замечания

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

Постановка задачи
Одним из практических применений компьютерной трехмерной графики является визуализация работы робототехнических систем.
При создании новых автоматов и роботов проектировщик нуждается в средствах визуализации для того, чтобы еще до воплощения проектируемой системы "в железе" увидеть своими глазами, как она будет функционировать в реальности.
Примеры программ, которые мы разберем в этой главе, конечно, далеки от того, чтобы решать подобные задачи в полном объеме, реальные автоматы здесь представлены весьма схематично. Однако знакомство с этими примерами даст представление о том, как пишутся подобные программы, и поможет получить опыт, необходимый для проектирования действительно сложных систем.
Первый пример - программа визуализации работы автомата по установке Уплотнителей. Это схематическое изображение реального устройства, включающего в себя питатель, наполняемый уплотнителями, в нижней части питателя расположен шибер, приводимый в движение штоком пневмоцилиндpa. Детали, на которые устанавливаются уплотнители, располагаются на шести спутниках, закрепленных на поворотном рабочем столе.
Сценарий фильма состоит в следующем: необходимо отобразить вращение рабочего стола, при приближении очередного спутника к рабочей позиции вращение рабочего стола прекращается, шток поршня пневмоцилиндра перемещает шибер, который выталкивает уплотнение из стопки накопителя и устанавливает его на деталь.
На рис. 5.1 показан один из моментов работы программы.

Рис. 5.1. Работа программы визуализации робота

Пользователь может наблюдать работу установки из любой точки зрения. При нажатии на пробел наблюдатель приближается, а при нажатии на пробел одновременно с <Shift> наблюдатель удаляется в пространстве. Нажатие на клавиши 'X', 'Y' или 'Z' приводит к повороту системы по соответствующим осям, если же при этом удерживать <Shift>, вращение осуществляется в обратную сторону. Клавиша 'O' ответственна за отображение координатных осей, нажатие на клавишу 'P' позволяет управлять отображением площадки, символизирующей поверхность, на которой располагается установка. Клавиши 'R', 'G', 'B' и эти же клавиши с <Shift> позволяют регулировать цветовую насыщенность источника света. Нажатие на клавишу 'M' приводит к изменению оптических свойств материала, из которого "создана" установка, в программе заданы восемь материалов. Клавиши управления курсором позволяют манипулировать положением источника света в пространстве;
если нажать клавишу 'L', то в точке положения источника света рисуется небольшая сфера.
Эти и некоторые другие параметры отображения системы хранятся в файле конфигурации ARM.dat. Значение текущих установок запоминается в нем при нажатии на клавишу 'S'. При запуске приложения из файла считываются значения установок, так что пользователь всегда может наблюдать работу системы в привычной конфигурации.
Для того чтобы ничто не отвлекало пользователя и не мешало ему, приложение запускается в полноэкранном режиме, а на время работы приложения курсор не отображается.
Если нажать левую кнопку мыши, то курсор становится видимым и появляется всплывающее меню (рис. 5.2).

Рис. 5.2. Приложение снабжено всплывающим меню

Выбор пункта меню "Помощь" или нажатие клавиши <F1> приводит к выводу содержимого файла справки (рис. 5.3).

Рис. 5.3. Пользователь может воспользоваться справкой по работе с программой

Структура программы
Программа реализована в виде нескольких модулей Во-первых, это выполняемый модуль, ARM exe, самый главный файл комплекса, хранящий весь основной код Для работы он нуждается в файле InitRC.dll, динамической библиотеке, хранящей процедуры и данные, связанные с источником света Кроме этого, используются еще две библиотеки, About.dll и ParForm.dll В принципе, основной модуль комплекса может работать и без этих двух библиотек Первая из них предназначена для вывода окна "Об авторах", а вторая - для вывода диалогового окна по заданию параметров системы
В этом разделе мы разберем основные модули программного комплекса Подкаталог Ex01 содержит проект основного модуля и обязательную библиотеку InitRC.dll
Проект основного модуля не использует библиотеку классов Delphi, поэтому размер откомпилированного модуля занимает всего 34 Кбайта
Для экономии ресурсов запретим пользователю запускать несколько копий приложения При запуске приложения определяем, зарегистрированы ли в системе окна такого же класса, и если это так, то прекращаем программу:

If FindWindow (AppName, AppName) <> 0 then
Exit;

Это необходимо сделать самым первым действием программы Следующий шаг - задание высшего приоритета для процесса:

SetPriorityClass (GetCurrentProcess, HIGH_PRIORITY_CIASS);

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

Замечание
Возможно, более тактичным по отношению к пользователю будет предоставление ему возможности самостоятельно решать, стоит ли повышать приоритет процесса Тогда это действие можно оформить так If MessageBox (0,'Для более быстрой работы приложения все остальные процессы будут замедлены ', 'Внимание'1, MB_OKCANCEL) = idOK then SetPriorityClass (GetCurrentProcess, HIGH_PRIORITY_CLASS)

На следующем шаге определяем, доступна ли для использования динамическая библиотека InitRC.dll, без которой наше приложение не сможет работать В проекте описана соответствующая ссылка на библиотеку:

hcDHMaterials : THandle;

Значение этой величины - указатель на библиотеку Если он не может быть получен, информируем пользователя и прекращаем работу:

hcDHMaterials := LoadLibrary('InitRC');
If hcDHMaterials <= HINSTANCE_ERROR then begin
MessageBox (0, 'Невозможно загрузить файл библиотеки InitRC.dll',
'Ошибка инициализации программы', mb_OK);
Exit
end,

Ответ на вопрос, зачем потребовалось выносить в отдельную библиотеку процедуры инициализации источника света, мы дадим чуть позже. Посмотрим, что происходит дальше в нашем приложении Откройте модуль WinMain.pas, содержащий описание головной процедуры, и модуль WinProc.pas, хранящий описание оконной функции. В предыдущих примерах, построенных полностью на использовании функций API, нам не приходилось использовать всплывающее меню, а сейчас разберем, как обеспечивается его функционирование.
В программе должны быть введены целочисленные идентификаторы пунктов создаваемого меню, обязательно в блоке констант:

const // идентификаторы пунктов меню
id_param = 101; // пункт "Параметры"
id about = 102; // пункт "Об авторах"
id_close = 103; // пункт "Выход"
id_help = 104; // пункт "Помощь"

Для хранения ссылки на меню должна присутствовать переменная типа HMenu. В процедуре, соответствующей точке входа в программу, ссылка принимает ненулевое значение при вызове функции createPopupMenu, заполнение меню осуществляется вызовом функций AppendMenu:

MenuPopup := CreatePopupMenu;
If MenuPopup <> 0 then begin
AppendMenu (MenuPopup, MF_Enabled, id_help, '&Помощь');
AppendMenu (MenuPopup, MF_Enabled, id_param, '&Параметры');
AppendMenu (MenuPopup, MF_Enabled, id_about, ' &0б авторах');
AppendMenu (MenuPopup, MF_Enabled, id_close, &Выход');
end;

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

wm_LButtonDown :
begin
ShowCursor (True);
TrackPopupMenu (MenuPopup, TPM_LEFTBUTTON,10,10,0,Window, nil) ;
end;

На время функционирования меню включается отображение курсора, чтобы пользователю не пришлось осуществлять выбор пунктов вслепую. Обработка выбора, произведенного пользователем, связана с сообщением WM_COMMAND, параметр wParam такого сообщения содержит значение, указывающее на сделанный выбор:

wm_Command : // всплывающее меню
begin
case wParam of // выбранный пункт меню
id_param : CreateParWindow; // "Параметры"
id_help : WinHelp(Window, 'ARM', HELP_CONTENTS, 0); // "Помощь"
// "Выход"
id_close : SendMessage (Window, wm_Destroy, wParam, IParam);
id_about : About; // "Об авторах"
end; // case
// рисовать ли курсор
If flgCursor = False then ShowCursor (False)
else ShowCursor (True);
end; // wm_coramand

При завершении работы приложения память, ассоциированную с меню, необходимо освободить:

DestroyMenu (MenuPopup);

Из этого же фрагмента вы можете увидеть, что вывод справки осуществляется вызовом функции WinHelp.
Поскольку справка может вызываться по выбору пункта меня и по нажатию клавиши <F1>, обратите внимание, что нажатию этой клавиши в операционной системе соответствует отдельное сообщение WM_HELP.
Приложение работает в полноэкранном режиме, что обеспечивается заданием стиля окна как

ws_Visible or ws_PopUp or ws_EX_TopMost,

что соответствует окну без рамки и без границ, располагающемуся поверх остальных окон Обработчик сообщения WM_CREATE начинается с того, что окно развертывается на весь экран - приложение посылает себе сообщение "развернуть окно"-

SendMessage (Window, WM_SYSCOMMAND, SC_MAXIMIZE, 0) ;

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

После этого происходит обращение к процедуре, считывающей значения Установок и загружающей процедуры из библиотеки InitRC.dll. Поясним, как это делается, на примере процедуры инициализации источника света Введен пользовательский тип:

TlnitializeRC = procedure stdcall;

Затем необходимо установить адрес этой процедуры в динамической библиотеке:

InitializeRC := GetProcAddress (hcDHMaterials, 'InitializeRC1);

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

begin // используется в case
// очистка буфера цвета и буфера глубины
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER__BIT);

glPushMatrix; // запомнили текущую систему координат
glRotatef (AngleXYZ [I], 1, О, 0);
glRotatef (AngleXYZ [2], 0, 1, 0);
glRotatef (AngleXYZ [3], 0, 0, 1);

glPushMatrix; // запомнили текущую систему координат - 0,0
If flgSquare then glCallList (4); // рисуем площадку
If flgOc then OcXYZ; // рисуем оси
If flgLight -then begin // рисуем источник света
glPushMatrix;
glTranslatef (PLPosition^ [1], PLPosition^[2], PLPosition^[3]);
gluSphere (ObjSphere, 0.01, 5, 5) ;
glPopMatnx;
end;

glCallList (11); // список - основание накопителя
glCallList (1) ; // штыри накопителя
// стопка прокладок
glTranslatef (0.1, -0.1, 0.0);
glEnable (GL_TEXTURE_1D); //на цилиндр накладывается текстура
gluCylinder (ObjCylinder, 0.125, 0.125, hStopki, 50, 50);
// последний уплотнитель в стопке
glTranslatef (0.0, 0.0, hStopki);
glCallList (5);
glDisable (GL_TEXTURE_1D);
// рисуем крышку накопителя
glTranslatef (0.0, 0.0, 1.5 - hStopki);
glCallList (10);
// рисуем пневмоцилиндр
glTranslatef (0.15, 0.0, -1.725);
glRotatef (90.0, 0.0, 1.0, 0.0);
glCallList (6);
glRotatef (-90.0, 0.0, 1.0, 0.0);
glTranslatef (-1.4, 0.0, 0.0);
// рисуем штырь пневмоцилиндра
If not (flgRotation) then begin // флаг, вращать ли стол
If wrkl = 0 then begin
hStopki := hStopki - 0.025; // уменьшить стопку
If hStopki < 0 then hStopki := 1; // стопка закончилась
end;
glPushMatrix;
glTranslatef (0.9, 0.0, 0.0);
glRotatef (90.0, 0.0, 1.0, 0.0);
glCallList (8); // список - штырь пневмоцилиндра
glPopMatrix;
end;
// рисуем шибер
If flgRotation // флаг, вращать ли стол
then glTranslatef (1.25, 0.0, 0.0)
else begin
glTranslatef (0.75, 0.0, 0.0);
Inc (wrkl); end;
glRotatef (90.0, 0.0, 1.0, 0.0); // шибер - кубик
glCallList (9);
If (not flgRotation) and (wrkl = 4) then begin // пауза закончилась
flgRotation := True;
Angle := 0;
wrkl := 0;
end;
glPopMatrix; // текущая точка - 0, 0
glCallList (7); // ось рабочего стола
glRotatef (90.0, 0.0, 1.0, 0.0);
If flgRotation then // флаг, вращать ли стол
glRotatef ( Angle, 1.0, 0.0, 0.0);
glCallList (2); // шесть цилиндров
glRotatef (-90.0, 0.0, 1.0, 0.0); // систему координат - назад
glRotatef (-30.0, 0.0, 0.0, 1.0); // для соответствия с кубиками
// список - шесть кубиков - деталь
glCallList (3) ;
glPopMatrix; // конец работы
SwapBuffers(DC);
end;

Стоит обратить внимание, что стопка уплотнителей симулируется наложением текстуры на цилиндр, поскольку прорисовка двух десятков цилиндров приведет к сильной потере производительности В примере используется одномерная текстура:

const // параметры текстуры
TexImageWidth = 64;
TexParams : Array [0..3] of GLfloat = (0.0, 0.0, 1.0, 0.0);
var
Texlmage : Array [1 .. 3 * TexImageWidth] of GLuByte;
procedure MakeTexImage;
begin 3 := 1;
While з < TexImageWidth * 3 do begin
Texlmage [3] := 248; // красный
Texlmage [3 + I] := 150; // зеленый
Texlmage [3 + 2] := 41; // синий
Texlmage [3 + 3] := 205; // красный
Texlmage [3 + 4] := 52; // зеленый
Texlmage [3 + 5] := 24; // синий
Inc (3, 6);
end;
glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL__REPEAT);
glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER7 GL NEAREST);
glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE~MIN_FILTER, GL_NEAREST);
glTexImagelD (GL_TEXTURE_1D, 0, 3, TexImageWidth, 0, GL_RGB,
GL_UNSIGNED_BYTE, @Texlmage);
glEnable (GL_TEXTURE_GEN_S);
// чтобы полоски не размывались
glTexGeni (GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
// поворачиваем полоски поперек цилиндра
glTexGenfv (GL_S, GL_OBJECT_PLANE, @TexParams);
end;

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

procedure OcXYZ; // Оси координат
begin
glColorSf (О, 1, 0);
glBegin (GL_LINES);
glVertex3f (0, 0, 0);
glVertexSf (3, 0, 0);
glVertex3f (0, 0, 0);
glVertexSf (0, 2, 0);
glVertexSf (0, 0, 0);
glVertex3f (0, 0, 3);
glEnd;
// буква X
glBegin (GL_LINES);
glVertexSf (3.1, -0.2, 0.5);
glVertexSf (3.1, 0.2, 0.1);
glVertexSf (3.1, -0.2, 0.1);
glVertexSf (3.1, 0.2, 0.5);
glEnd;
// буква Y
glBegin (GL_LINES);
glVertexSf (0.0, 2.1, 0.0);
glVertexSf (0.0, 2.1, -0.1);
glVertexSf (0.0, 2.1, 0.0);
glVertexSf (0.1, 2.1, 0.1);
glVertexSf (0.0, 2.1, 0.0);
glVertexSf (-0.1, 2.1, 0.1);
glEnd;
// буква Z
glBegin (GL_LINES);
glVertexSf (0.1, -0.1, 3.1);
glVertexSf (-0.1, -0.1, 3.1);
glVertex3f (0.1, 0.1, 3.1);
glVertexSf (-0.1, 0.1, 3.1);
glVertexSf (-0.1, -0.1, 3.1);
glVertexSf (0.1, 0.1, 3.1);
glEnd;
// Восстанавливаем значение текущего цвета
glColor3f (Colors [1], Colors [2], Colors [3]);
end;

Модули приложения
Головной модуль программы спроектирован только с использованием функций API, что обеспечивает миниатюрность откомпилированного файла. Желание облегчить взаимодействие пользователя с программой привело меня к мысли о необходимости включения диалогового окна, в котором пользователь мог бы удобным и привычным для себя образом задавать конфигурацию системы. Если бы я и этот модуль создавал без VCL, эта книга никогда не была бы написана - страшно даже представить объем работы по кодированию диалоговых окон вручную. Поэтому вспомогательный модуль я разработал обычным для Delphi способом, а для взаимодействия с головной программой использовал подход, основанный на вызове динамической библиотеки.
Окно "Параметры системы" снабжено интерфейсными элементами, позволяющими задавать установки. Это окно имеет также кнопку "Применить", реализующую стандартное для подобных диалогов действие. Пользователь имеет возможность попробовать, как будет происходить работа системы при выбранных значениях установок. Вот эта самая кнопка потребовала значительного усложнения структуры программы.
Начнем изучение модулей комплекса с самого неответственного - с модуля About.dll, содержащего окно "Об авторах" (рис. 5.5).

Рис. 5.5. "Все права зарезервированы"

Подкаталог Ех02 содержит соответствующий проект. Логотип автора приложения представляет собой стилизацию логотипа библиотеки OpenGL. Чтобы впоследствии поместить окно в динамическую библиотеку, в разделе interface модуля формы окна "Об авторах" я поместил следующую строку с forward-описанием процедуры:

procedure AboutForm; stdcall; export;

Код процедуры, это уже в разделе implementation модуля unitl.pas, совсем простой - создание и отображение окна:

procedure AboutForm; stdcall; export;
begin
Forml := TForml.Create ( Application);
Forml.ShowModal;
end;

Итак, модуль unitl.pas содержит описание экспортируемой функции AboutForm, связанной с отображением окна "Об авторах".
Проект About.dpr из подкаталога Ех02 предназначен для компоновки файла динамической библиотеки About.dll:

library About; uses
Unitl in 'Unitl.pas'; exports
AboutForm; // функция, размещаемая в DLL
begin
end.

Откомпилируйте этот проект, выбрав соответствующий пункт меню среды Delphi или нажав комбинацию клавиш <Ctrl>+<F9>.

Замечание
Обращаю ваше внимание, что запускать проекты с заголовком library бессмысленно, невозможно "запустить" динамическую библиотеку.

После компиляции получается файл About.dll, который необходимо переместить в каталог приложения, использующего эту библиотеку, то есть туда же, где располагается модуль ARM.exe. Головной модуль при выборе пользователем пункта меню "Об авторах" обращается к процедуре AboutForm, загружаемой из библиотеки. Если при загрузке функции происходит ошибка, исключительная ситуация, приложение ее снимает. Я оставляю пользователя в неведении по поводу произошедшей аварии в силу ее малозначительности.
В модуле About.pas головного проекта содержится описание соответствующих типов и процедуры:

type
TAboutForm = procedure stdcall; // тип загружаемой из dll процедуры
var
AboutForm : TAboutForm; // переменная процедурного типа
procedure About; // вспомогательная процедура
begin
try
t // режим защиты от ошибок
hCDll := LoadLibrary ('About'); // ссылка на соответствующую библиотеку
If hCDll <= HINSTANCE_ERROR then begin // ошибка загрузки dll
hCDll := NULL; // освобождаем память
Exit // остальные действия не делать
end else // пытаемся получить адрес процедуры в dll
AboutForm := GetProcAddress(hCDll, 'AboutForm');
If not Assigned (AboutForm)
then Exit // ошибка, dll не содержит такую процедуру
else AboutForm; // все в порядке, запускаем процедуру из dll
If not hCDll = NULL then
begin
FreeLibrary (hCDll); // освобождение памяти
hCdll := NULL;
end;
except Exit // в случае ошибки снять аварийную ситуацию и закончить
end; // try
end;

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

Обмен данными с DLL
Напомню, что головной модуль не использует библиотеку классов Delphi, однако проектировать без визуальных средств диалоговые окна - дело слишком тяжелое. Поэтому будем использовать для их реализации все мощные и удобные средства, предоставляемые Delphi специально для этих целей. Диалоговые окна размещены в динамических библиотеках, но должны иметь общие с головной программой данные. Решить проблему обмена данными с различными модулями можно различными путями, в частности, в этом примере используются указатели на данные. Например, диалоговое окно "Параметры системы" имеет флажок "Площадка", с помощью которого пользователь может задавать режим отображения этого объекта. После задания режима при воспроизведении кадра необходимо соответствующим образом отобразить текущие значения установки. Значение установки хранит в головном модуле булевская переменная-флажок figSquare; если она равна true, то при перерисовке кадра вызывается дисплейный список площадки:

If figSquare then glCallList (4); // рисуем площадку

Откройте модуль Unit1.pas в подкаталоге Ех0З и посмотрите forward-описание процедуры ParamForm, размещаемой в динамической библиотеке ParForm.dll, которая связана с отображением и функционированием диалогового окна "Параметры системы". Первые четыре аргумента этой процедуры - указатели на переменные булевского типа; каждая из них задает определенный флаг, управляющий режимом отображения осей, площадки, источника света или указателя курсора. При вызове этой процедуры из головного модуля в качестве фактических аргументов процедуры передаются ссылки на соответствующие переменные:

ParamForm (@flgOc, @flgSquare, SflgLight, @flgCursor, ...

Вы можете увидеть это в модуле ParForm.pas головного проекта ARM.dpr. Теперь при нажатии кнопки "Применить" окна "Параметры системы", как и при закрытии этого окна, переменной по заданному адресу устанавливается соответствующее значение:

If Forml.CheckBoxSquare.Checked
then wrkPFlagSquare^ := True
else wrkPFlagSquare^ := False;

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

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

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

procedure GetData (var PMaterials : PMateria1;
var PLPosition : PArray4D;
var PFAmbient : PArray4D;
var PLDirection : PArraySD); export; stdcall;
begin
PMaterials := @Materials;
PLPosition := @LPosition;
PFAmbient := OFAmbient;
PLDirection := SLDirection;
end;

В подкаталоге Ех04 я поместил исходные файлы пользовательской библиотеки InitRC. Обратите внимание, что "головная часть" проекта динамической библиотеки соответствует этапу ее инициализации. В данном примере на этом этапе я задаю оптические свойства материала конструкции и инициализирую переменные:

begin // инициализация библиотеки
Materials := 1;
Material [1] := @AmbBronza;
Material [2] := @DifBronza;
Material [3] := SSpecBronza;
// цвет материала и диффузное отражение материала - значения из массива
glMaterialfv(GL_FRONT, GL_AMBIENT, Material [1]);
glMaterialfv(GL_FRONT, GL_DIFFUSE, Material [2]);
glMaterialfv(GL_FRONT, GL_SPECULAR, Material [3] ) ;
glMaterialf (GL_FRONT, GL_SHININESS, 51.2);
end.

Дополнительные замечания
Приложению требуется несколько секунд для завершения работы. Чтобы у пользователя не сложилось впечатление, что система зависла, по окончании работы я убираю (минимизирую) окно приложения в панель задач:

postMessage(Window, WM_SYSCOMMAND, SC_MINIMIZE, 0);

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

Замечание
Здесь я сознательно не использовал операторы Delphi as и is, упрощающие и сокращающие код, поскольку их использование заметно замедляет работу приложения

Серьезным упреком к процедуре проверки может быть то, что для перевода вещественных чисел в строку и наоборот используются процедуры val и str, не учитывающие, какой разделитель дробной части установлен в системе, поэтому могут возникнуть неудобства, если пользователь сильно привык к запятой в качестве такого разделителя
При проверке содержимого каждого поля в случае допущенной пользователем ошибки применяется "тихая" исключительная ситуация - стандартный прием:

If edtFAmbientR.Text = '' then raise EAbort.Create ('Заполните все поля1');
Val (edtFAmbientR.Text, dW, iW) ;
If (iW<>0) then raise EAbort.Create ('Числовые данные введены с ошибкой1');

Обращение к процедуре проверки осуществляется в защищенном блоке, при возникновении ошибки класса EAbort пользователь получает соответствующую информацию, и попытка применить введенные значения прекращается.

try
Forml.Proverka except
on E : EAbort do With Forml do begin
TabbedNotebookl.Visible := False;
btnApply.Visible := False;
btnCancel.Visible := False;
btnOK.Visible := False;
btnError.Visible := True;
IblError.Caption := E.Message;
IblError.Visible := True;
Exit;
// ошибка, данные применять нельзя
end;// with
end; // try

Разобранную программу можно использовать в качестве шаблона для проектирования других приложений подобного типа. Подкаталог Ех05 содержит еще один проект по визуализации автоматов, также схематично демонстрирующий работу реальной установки
(рис. 5.6).

Рис. 5.6. Еще один пример на визуализацию работы роботов

Программа помимо сценария кадра ничем не отличается от разобранного ранее в этой главе примера: каркас проекта не изменился, только управление дополнилось клавишами '<' и '>', с помощью которых можно сдвигать точку зрения в пространстве.
На первый, поверхностный, взгляд может показаться, что здесь решается более простая задача по сравнению с первоначальным проектом, работа не такая объемная и значительно менее зрелищная. Однако самое интересное кроется как раз в мелких деталях.
Болты, ограничивающие движение верхней детали системы, "продеты сквозь отверстия в плите. Для рисования этих отверстий верхнюю и нижнюю часть плиты пришлось разбить на десять отдельных частей. Сами отверстия рисуются по принципам, знакомым нам по главе 2, когда мы учились рисовать дырки. На рис. 5.7 приведен вариант воспроизведения плиты в каркасном режиме, когда хорошо выделены все секторы, на которые на самом деле разбита поверхность.

Рис. 5.7. Для того чтобы нарисовать отверстия в плите, пришлось потрудиться

Замечание
Использование буфера трафарета привело бы здесь к потере производительности в десятки раз. Напоминаю, что есть еще один способ решения таких задач - использование tess-объектов.

Код воспроизведения кадра, как и в предыдущем примере, становится сравнительно кратким после того, как вся система поэлементно описана в дисплейных списках:

begin // используется в case
// очистка буфера цвета и буфера глубины
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER BIT);
glPushMatrix; // запомнили текущую систему координат - 0,0
// Установочный сдвиг
glTranslatef(AddXYZ [1], AddXYZ [2], AddXYZ [3] -7.0);
glRotatef (AngleXYZ [1], 1, О, О);
glRotatef (AngleXYZ [2], 0, 1, 0);
glRotatef (AngleXYZ [3], 0, 0, 1);
If flgSquare then glCallList (1); // рисуем площадку (плоскость узла)
If flgOc then OcXYZ; // рисуем оси
If flgLight then begin // рисуем источник света
glTranslatef (PLPosition^ [1], PLPosition^ [2], PLPositaon^[3]);
gluSphere (ObjSphere, 0.01, 5, 5);
glTranslatef (-PLPosition^ [1], —PLPosition^ [2], —PLPosition^ [3]);
end;
glScalef (CoeffX, CoeffY, CoeffZ);
glTranslatef (0.0, 0.0, SmallB);
glCallList (3); // пружина
glCallList (10); // дырки в плите под болты
glCallList (5); // плита
glRotatef (AngleX, 1.0, 0.0, 0.0);
glRotatef (AngleY, 0.0, 1.0, 0.0);
glTranslatef (0.0, 0.0, Smallh);
glCallList (4); // диск
glCallList (8); // первый болт
glCallList (9); // второй болт
glRotatef (AngleZ, 0.0, 0.0, 1.0);
glCallList (2); // шпильковерт со шпинделем
glCallList (6); // патрон
glCallList (7); //деталь
glPopMatrix;
// конец работы
SwapBuffers(DC);
end;

Конечно, вызываемые подряд списки можно также объединить, и после этого код вообще уложится в десяток строк.


OpenGL в Delphi - ГЛАВА 6 Создаем свой редактор

ГЛАВА 6 Создаем свой редактор

Выбор элементов
Буфер выбора
Вывод текста
Связь экранных координат с пространственными
Режим обратной связи
Трансформация объектов
Постановка задачи
Структура программы
Несколько советов

Эта глава посвящена разработке графического редактора на основе OpenGL. Она будет особенно полезна программистам, занимающимся созданием CAD-систем и систем визуализации ввода исходных данных для подобных приложений. Рассматриваются также вопросы, связанные с выбором объектов и выводом символов в OpenGL.
Примеры располагаются на дискете в каталоге Chapter6.

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

ColorToGL (Canvas.Pixels [X,Y], R, G, B) ;
If (R о 0) and (B = 0) then
ShowMessage ('Выбран красный треугольник');

Здесь для определения составляющих цвета пиксела вызывается пользовательская процедура, знакомая по главе 1.

glRotatef (AngleXYZ [1], 1, 0, 0);
glRotatef (AngleXYZ [2], 0, 1, 0);
glRotatef (AngleXYZ [3], 0, 0, 1);
If flgSquare then glCallList (1); // рисуем площадку (плоскость узла)
If flgOc then OcXYZ; // рисуем оси
If flgLight then begin // рисуем источник света
glTranslatef (PLPosition^ [1], PLPosition^ [2], PLPosition^ [3]);
gluSphere (ObjSphere, 0.01, 5, 5);
glTranslatef (-PLPosition^ [1], -PLPosition^ [2], -PLPosition^ [3]);
end;
glScalef (CoeffX, CoeffY, CoeffZ);
glTranslatef (0.0, 0.0, SmallB);
glCallList (3); // пружина
glCallList (10); // дырки в плите под болты
glCallList (5); // плита
glRotatef (AngleX, 1.0, 0.0, 0.0);
glRotatef (AngleY, 0.0, 1.0, 0.0);
glTranslatef (0.0, 0.0, Smallh);
glCallList (4); // диск
glCallList (8); // первый болт
glCallList (9); // второй болт
glRotatef (AngleZ, 0.0, 0.0, 1.0);
glCallList (2); // шпильковерт со шпинделем
glCallList (6); // патрон
glCallList (7); //деталь
glPopMatrix;
// конец работы SwapBuffers(DC);
end;

Конечно, вызываемые подряд списки можно также объединить, и после этого код вообще уложится в десяток строк.
В примере для определения цвета пиксела под курсором используются средства Delphi - прямое обращение к цвету пиксела формы.
В проекте из подкаталога Ех02 делается все то же самое, но цвет пиксела определяется с помощью команды OpenGL:

var
wrk : Array [0..2] of GLUbyte; begin
glReadPixels (X, Y, 1, 1, GL_RGB, GLJJNSIGNED_BYTE, @wrk);
If (wrk [0] о 0) and (wrk [2] = 0) then
ShowMessage ('Выбран красный треугольник')
else
If (wrk [0] = 0) and (wrk [2] <> 0) then
ShowMessage ('Выбран синий треугольник')
else
ShowMessage ('Ничего не выбрано');
end;

Выбор объектов, основанный на определении цвета пиксела под курсором, немногим отличается от простого анализа координат точки выбора. Достоинства такого метода - простота и высокая скорость. Но есть и недостатки:

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

Следующий метод выбора похож на предыдущий: элементы, раскрашенные в уникальные цвета, рисуются в заднем буфере кадра, без вызова команды SwapBuffers. Для выбора элементов анализируем цвет заднего буфера в позиции, в которой осуществляется выбор. В заднем буфере элементы рисуются без текстуры и при выключенном источнике света, чтобы не портить чистые цвета.
Обратимся для ясности к проекту из подкаталога Ех03, где рисуются два треугольника одинакового цвета, при нажатии кнопки сообщается, какой треугольник выбран, левый или правый.
В процедуре перерисовки окна после команды SwapBuffers код рисования треугольников повторяется, однако каждый треугольник раскрашивается в уникальные цвета. Цвет фона для простоты дальнейших манипуляций задается черным.
Обработка нажатия кнопки начинается так:

wglMakeCurrent(Canvas.Handle, hrc);
glReadPixels(X, ClientHeight - Y, I, I, GL_RGB, GL_UNSIGNED_BYTE, @Pixel);
If (Pixel [0] <> 0) and (Pixel [2] = 0)
then ShowMessage ('Выбран левый треугольник');

To есть считываем пиксел в текущем, заднем, буфере кадра в позиции курсора, для чего необходимо предварительно установить контекст воспроизведения. Массив, хранящий цвета пиксела - массив трех элементов типа GLbyte.
Для разнообразия в этом примере я не занимаю контекст воспроизведения один раз на все время работы приложения, а делаю это дважды: один раз при перерисовке окна для воспроизведения сцены и второй раз при нажатии кнопки мыши для того, чтобы воспользоваться командами OpenGL чтения пиксела. Каждый раз после работы контекст, конечно, освобождается.
Чтобы не сложилось впечатление, что такой метод применим только для плоскостных построений, предлагаю посмотреть пример из подкаталога Ех04, где рисуются сфера и конус из одного материала и осуществляется выбор между ними. Можете перенести вызов команды SwapBuffers в конец кода перерисовки окна, чтобы увидеть, как сцена выглядит в заднем буфере.

Буфер выбора
В режиме выбора OpenGL возвращает так называемые записи нажатия (hit records), которые содержат информацию о выбранном элементе. Для идентификации элементов они должны быть поименованы, именование. Элементов осуществляется С помощью Команд glLoadName или glPushName. Имя объекта в OpenGL - любое целое число, которое позволяет уникально идентифицировать каждый выбранный элемент. OpenGL хранит имена в стеке имен.
Для включения режима выбора необходимо вызвать команду glRenderMode с аргументом GL_SELECTION. Однако прежде чем сделать это, требуется определить буфер вывода, куда будут помещаться записи нажатия. При нахождении в режиме выбора содержимое заднего буфера кадра закрыто и не может быть изменено.
Библиотека OpenGL будет возвращать запись нажатия для каждого объекта, находящегося в отображаемом объеме. Для выбора только среди объектов, находящихся под курсором, необходимо изменить отображаемый объем. Библиотека glu содержит команду, позволяющую это сделать - gluPickMatrix, которая создает небольшой отображаемый объем около координат курсора, передаваемых в команду в качестве параметров. После задания области вывода можно только выбирать объекты. Напоминаю, что перед рисованием. Объектов вызываются команды glLoadName ИЛИ glPushName.
После осуществления выбора необходимо выйти из режима выбора вызовом команды glRenderMode с аргументом GL_RENDER. С этого момента команда будет возвращать число записей нажатия, и буфер выбора может быть проанализирован. Буфер выбора представляет собой массив, где каждая запись нажатия содержит следующие элементы:

  • число имен в стеке имен на момент нажатия;
  • минимум и максимум оконной координаты Z примитивов в отображаемом объеме на последнее нажатие; эти значения лежат в пределах от нуля до единицы, но перед выводом в буфер выбора умножаются на 232-1;
  • фактическое содержание буфера имен на момент нажатия, начиная с верхнего.

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

  • они входят в стандартную часть OpenGL и не требуют дополнительного кодирования;
  • буфер выбора предоставляет гибкий путь для выбора группированных или иерархических элементов

Однако есть и недостатки:

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

Обратимся к простому примеру из подкаталога Ex05, чтобы уяснить использование техники выбора в OpenGL. Ha экране рисуются два треугольника - синий и красный. При нажатии кнопки мыши на поверхности окна выдается сообщение, на каком треугольнике сделан выбор, либо выводится фраза "Пустое место", если под курсором нет ни одного треугольника.
Само построение треугольников вынесено в процедуру, начало описания которой выглядит так:

procedure Render (mode : GLenum); // параметр - режим (выбора/рисования)
begin
// красный треугольник
If mode = GL_SELECT then glLoadName (1); // называем именем 1
glColor3f (1.0, 0.0, 0.0);

Процедурой Render будем пользоваться для собственно построения изображения и для выбора объекта, режим передается в качестве параметра В случае, если действует режим выбора, перед рисованием объекта загружаем его
имя командой glLoadName, аргумент которой - целое число, задаваемое разработчиком. В примере для первого, красного треугольника, загружаем единицу, для второго треугольника загружаем двойку.
При нажатии кнопки мыши координаты курсора передаются в функцию DoSelect. Это важная часть программы, поэтому приведем описание функции целиком.

function DoSelect(x : GLint; у : GLint) : GLint;
var hits : GLint;
Begin
glRenderMode(GL_SELECT); // включаем режим выбора
// режим выбора нужен для работы следующих команд
glInitNames; // инициализация стека имен
glPushName(0); // помещение имени в стек имен
glLoadIdentity;
gluPickMatrix(x, windH - у, 2, 2, @vp);
Render(GL_SELECT); // рисуем объекты с именованием объектов
hits := glRenderMode(GL_SELECT);
if hits <= 0
then Result := -1
else Result := SelectBuf [(hits - 1) * 4 + 3];
end;

Функция начинается с включения режима выбора. Команда glInitNames очищает стек имен, команда glPushName помещает аргумент в стек имен. Значение вершины стека заменяется потом на аргумент команды glLoadName.
Команда gluPickMatrix задает область выбора. Первые два аргумента - центр области выбора, в них передаем координаты курсора. Следующие два аргумента задают размер области в пикселах, здесь задаем размер области 2x2. Последний аргумент - указатель на массив, хранящий текущую матрицу. Ее запоминаем в обработчике события WM_SIZE при каждом изменении размеров окна:

glGetIntegerv(GL_VIEWPORT, @vp);

После этих приготовлений воспроизводим картинку параллельно с загрузкой имен в стек.
Последние строки - собственно выбор: снова задаем режим выбора и анализируем возвращаемый результат. Здесь SelectBuf - массив буфера выборa, подготовленный в начале работы приложения:

glSelectBuffer{MaxSelect, @SelectBuf); // создание буфера выбора

Первый аргумент - размер буфера выбора, в примере задан равным 4 - ровно под один объект, поскольку в проекте выбирается и перекрашивается один, самый верхний (ближайший к наблюдателю) объект.
Более элегантный код для этого действия записывается так:

glSelectBuffer(SizeOf(SelectBuf), @SelectBuf);

Выражение для извлечения имени элемента ((hits - 1> * 4 + 3) в случае, если нам нужно получить имя только последнего, верхнего объекта, можно заменить на просто 3, тем более что в данном примере больше одного элемента в буфер не помещается. Как следует из документации и из вводной части этого раздела, номер выбранного элемента располагается четвертым в буфере выбора, поэтому, если требуется извлечь имя только одного, самого верхнего объекта, можно использовать явное выражение для получения номера выбранного элемента:

Result := SelectBuf [3];

Замечание
В этом примере для определения имени выбранного элемента команда glRenderMode вызывается с аргументом GL_SELECT Согласно документации, в этом случае команда возвращает количество записей нажатия, помещенных в буфер выбора, а при вызове с аргументом GL_RENDER эта команда возвращает всегда ноль. Однако в примере на выбор элемента из пакета SDK при выборе элемента эта команда вызывается именно с аргументом GL_RENDER В нашем примере не будет разницы в результате, если аргументом glRenderMode брать любую из этих двух констант, однако некоторые последующие проекты будут корректно работать только при значении аргумента GL_RENDER Это следует понимать как возвращение обычного режима воспроизведения - воспроизведения в буфер кадра.

Рассмотрим пример из подкаталога Ех06. На экране рисуется двадцать треугольников со случайными координатами и цветом, при нажатии кнопки мыши на каком-либо из них треугольник перекрашивается (рис. 6.1).

Рис. 6.1.В примере осуществляется выбор среди двадцати объектов со случайными координатами

При проектировании собственного модельера мы возьмем из этого примера некоторые приемы работы с объектами.
В программе вводится собственный тип для используемых геометрических объектов:

type
TGLObject = record // объект - треугольник
{вершины треугольника) vl : Array [0..1] of GLfloat;
v2 : Array [0..1] of GLfloat;
v3 : Array [0..1] of GLfloat;
color : Array [0..2] of GLfloat; // цвет объекта
end;

Объект, треугольник, состоит из трех вершин, для хранения координат каждой из которых используем массивы из двух вещественных чисел, X и Y. Цвет каждого объекта записывается в массиве из трех вещественных чисел.
Система объектов хранится в массиве:

Objects : Array [O..MaxObjs - 1] of TGLObject;

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

If mode = GL_SELECT then glLoadName(i.); // загрузка очередного имени

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

Замечание
Очень важно подчеркнуть, что все, что воспроизводится между очередными вызовами glLoadName, считается одноименным Это очень удобно для выбора между комплексными геометрическими объектами, но отсутствие возможности явной командой прекратить именовать выводимые объекты требует продуманного подхода к порядку вывода объектов то, что не должно участвовать в выборе элементов, необходимо выводить до первого вызова glLoadName или не выводить вообще.

Разберем еще один полезный пример по этой теме, проект из подкаталога Ех07. Здесь выводится поверхность, построенная на основе 229 патчей, образующих модель, предоставленную Геннадием Обуховым (вообще-то картинка немного жутковата, рис. 6.2).

Рис. 6.2. Каждый патч поверхности можно перекрасить

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

type
PPointArray = ^TPointArray; // динамический массив модели
TPointArray = Array [0..0] of AVector;
ColorArray = Array [0..2] of GLfloat; // массив цвета патча
PColorArray = ^TColorArray; // указатель на массив
TColorArray = Array [0..0] of ColorArray; // собственно массив

При считывании модели создаем массив цвета патчей, массив цвета для каждого патча заполняем красным:

procedure TfrmGL.Init_Surface;
var
f : TextFile;
i, j : Integer;
begin
AssignFile (f, 'Eye.txt');
ReSet (f);
ReadLn (f, numpoint); // количество патчей в модели
GetMem (Model, (numpoint + 1) * SizeOf (AVector)); // выделяем память
// создаем динамический массив цвета патчей
GetMem (PatchColor, (numpoint + 1) * SizeOf (ColorArray));
For i := 0 to numpoint do begin
For j := 0 to 15 do begin // считываем очередной патч модели
ReadLn (f. Model [i][j].x); // каждое число с новой строки
ReadLn (f, Model [i] [3].у);
ReadLn (f, Model [i][]].z);
end;
// массив цвета очередного патча
PatchColor [i][0] := 1.0; // красный
PatchColor [i][1] := 0.0;
PatchColor [i][2] := 0.0;
end;
CloseFile (f);
end;

Вывод собственно модели вынесен в отдельную процедуру, при обращении к которой с аргументом GL_SELECT каждый патч именуется, в качестве имени берется его порядковый номер:

procedure TfrmGL.Render (Mode : GLEnum);
var
i : Integer;
begin
glPushMatrix; glScalef (2.5, 2.5, 2.5);
For i := 0 to numpoint do begin
If mode = GL_SELECT
then glLoadName (i) // именование воспроизводимого патча
else glColor3fv(@PatchColor[i]); // при выборе цвет безразличен
// построение очередного патча
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 4, 4, 0, 1, 16, 4, @model[i]);
glEvalMesh2(GL_FILL, 0, 4, 0, 4);
end;
glPopMatrix;
end;

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

hits := Deselect (X, Y) ;

И если номер объекта больше либо равен нулю, меняем цвет соответствующего патча и перерисовываем экран

PatchColor [hits][0] := 0.0;
PatchColor [hits][1] := 1.0;
PatchColor [hits][2] := 0.0;
InvalidateRect(Handle, nil, False);

Если же нажимать кнопку на пустом месте, то при движении курсора модель вращается, так что легко убедиться, что выбор осуществляется при любом положении точки зрения в пространстве
Обратите внимание, что размер буфера выбора я задал сравнительно большим, 128. Если брать его, как в предыдущих примерах, равным четырем, то приложение работает устойчиво, но при выходе из него появляется сообщение об ошибке, знакомое каждому программисту (рис 6 3).

Рис. 6.3. Малопонятный сбой в программе

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

If showPoints then begin
glScalef (2.5, 2.5, 2.5);
glDisable (GL_LIGHTING);
glBegin (GL_POINTS);
For i := 0 to numpoint do // цикл по патчам поверхности
For j := 0 to 15 do // цикл по узлам каждого патча
glVertexSf (model [i] [3 ] .x, model [i] [3 ] .y, model [i] [3 ]. z) ;
// предпочтительнее бы в векторной форме:
// glVertex3fv (@model[i][3]);
glEnd;
glEnable (GL_LIGHTING);
end;

Еще один пример на выбор объектов, проект из подкаталога Ех08 - здесь под курсором может оказаться несколько объектов
(рис 6. 4).

Рис. 6.4. Все объекты, располагающиеся под курсором, доступны для выбора, в том числе

Шесть кубиков по кругу пронумерованы от нуля до пяти, некоторые из них перекрывают друг друга в плоскости зрения При нажатии кнопки мыши с правой стороны экрана в компоненте класса TMemo выводится информация, сколько кубиков располагается под курсором, и последовательно выводятся их имена в порядке удаления от глаза наблюдателя Процедура выбора в этом примере отличается тем, что возвращает не номер верхнего элемента, а количество объектов под курсором:

Result := glRenderMode(GL_RENDER);

Имена объектов располагаются в буфере выбора через четыре, начиная с третьего элемента, поэтому для анализа содержимого буфера остается только последовательно считывать эти элементы

procedure TfrmGL.FormMouseDown(Sender: T0b3ect; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
hit, hits: GLUint;
begin
hits := DoSelect (X, Y); // объектов под курсором
Memol.Clear;
Memol.Lines.Add(Format('Объектов под курсором o %d',[hits]));
// считываем имена - каждый четвертый элемент массива, начиная с 3-го
For hit := 1 to hits do
Memol.Lines.Add(' Объект N" + IntToStr(hit) +
' Имя: ' + IntToStr(SelectBuf[(hit - 1)* 4 + 3]));
end;

Замечание
Соседние элементы массива, напоминаю, содержат значения буфера глубины отдельно для каждого объекта, но эти величины редко используются ведь порядок перечисления объектов и так предоставляет полную информацию о расположении объектов
и закрытые другими объектами

Этот пример полезен для решения задач по обработке серии элементов. Проект из подкаталога Ех09 представляет собой модификацию примера с перекрашиванием треугольников: количество объектов увеличено в десять раз, чтобы было больше перекрывающихся элементов. При нажатии кнопки мыши перекрашиваются все треугольники под курсором. Размер буфера выбора потребовалось увеличить, а функцию выбора переписать в процедуру, заканчивающуюся циклом по именам всех треугольников под курсором, где эти объекты перекрашиваются:

For hit := 1 to glRenderMode(GL_RENDER) do
RecolorTn(SelectBuf[(hit - 1) * 4 + 3]); // перекрашиваем объект

Вывод текста
Первое применение библиотеки OpenGL, которое я обнаружил на своем компьютере и с которого собственно и началось мое знакомство с ней - это экранная заставка "Объемный текст", поставляемая в составе операционной системы в наборе "Заставки OpenGL". Думаю, вам знакома эта заставка, выводящая заданный текст (по умолчанию - "OpenGL") или текущее время красивыми объемными буквами, шрифт которых можно менять. Поняв, что это не мультфильм, а результат работы программы, я загорелся желанием научиться делать что-нибудь подобное, после чего и началось мое увлекательное путешествие в мир OpenGL.
Выводить текст средствами OpenGL совсем несложно. Библиотека имеет готовую команду, строящую набор дисплейных списков для символов заданного шрифта типа TrueType - команду wgiuseFontOutimes. После подготовки списков при выводе остается только указать, какие списки (символы) нам необходимы.
Посмотрим проект из подкаталога Ех10, простейший пример на вывод текста (рис. 6.5).

Рис. 6.5. Выводить символы в OpenGL совсем несложно

Замечание
Обратите внимание, что при описании формата пикселов я явно задаю значение поля cDepthBits, иначе буквы покрываются ненужными точками.

При начале работы приложения вызывается команда, подготавливающая списки для каждого символа текущего шрифта формы:

wgiuseFontOutimes (Canvas.Handle, 0, 255, GLF_START_LIST, 0.0, 0.15,
WGL_FONT_POLYGONS, nil);

Замечание
В этом примере для получения контекста воспроизведения обязательно нужно использовать самостоятельно полученную ссылку на контекст устройства Если вместо DC использовать Canvas.Handle, вывод может получиться неустойчивым: при одном запуске приложения окно черное, а при следующем - все в порядке.

Цвет шрифта, установленного в окне, при выводе текста не учитывается, в этом отношении выводимые символы ничем не отличаются от прочих геометрических объектов. Свойства материала и источника света необходимо задавать самостоятельно.
Возможно, кому-то из читателей потребуется использовать вывод текста в приложениях, написанных без использования VCL. В примере из подкаталога Ex11 делается то же, что и в предыдущем, но шрифт отличается. Ну и, конечно, вырос размер кода:

var
hFontNew, hOldFont : HFONT;
// подготовка вывода текста FillChardf, SizeOf(lf), 0) ;
If.lfHeight = -28;
If.lfWeight = FW_NORMAL;
If.lfCharSet = ANSI_CHARSET;
If.IfOutPrecision = OUT_DEFAULT_PRECIS;
If.IfClipPrecision = CLIP_DEFAULT_PRECIS;
If.lfQuality = DEFAULT_QUALITY;
If.IfPitchAndFamily = FF_DONTCARE OR DEFAULT_PITCH;
Istrcpy (If.IfFaceName, 'Arial Cyr');
hFontNew := CreateFontlndirect(If);
hOldFont := SelectObject(DC,hFontNew);
wgiUseFontOutlines(DC, 0, 255, GLF_START_LIST, 0.0, 0.15,
WGL_FONT_POLYGONS, nil);
DeleteObject(SelectObject(DC,h01dFont));
DeleteObject(SelectObject(DC,hFontNew));

Рассмотрим команду wgiuseFontOutimes. Первый параметр - ссылка на контекст устройства, в котором должен быть установлен соответствующий шрифт. Второй и третий параметры задают интервал кодов символов, для которых будут строиться списки. Четвертый параметр задает базовое значение для идентификации списков - для каждого символа создается отдельный список, нумеруются они по порядку, начиная с задаваемого числа. Пятый параметр, называемый "отклонение", задает точность воспроизведения символов; чем меньше это число, тем аккуратнее получаются символы, за счет ресурсов, конечно. Шестой параметр, выдавливание, задает глубину получаемых символов в пространстве. Седьмой параметр определяет формат построения, линиями или многоугольниками. Последний, восьмой, параметр - указатель на массив специального типа TGLYPHMETRICSFLOAT или NULL, если эти величины не используются (в Delphi здесь необходимо задавать ml).
Думаю, вы заметили, что операция построения 256 списков сравнительно длительная, после запуска приложения на это тратится несколько секунд.
Для собственно вывода текста я прибегнул к небольшой уловке, написав следующую, хоть и небольшую, но отдельную, процедуру

procedure OutText (Litera : PChar);
begin
glListBase(GLF_START_LIST); // смещение для имен списков
// вызов списка для каждого символа
glCallLists(Length (Litera), GL_UNSIGNED_BYTE, Litera);
end;

Здесь скрыто использование преобразования типа выводимого символа в PChar, и вызов процедуры, например, OutText ('проба'), выглядит привычным образом. Вывод текста состоит из двух действий - команда giListBase задает базовое смещение для вызываемых списков, а команда glCallLists вызывает на выполнение списки, соответствующие выводимому тексту.

Замечание
Смещение имен списков в общем случае использовать не обязательно, обычно это делается для того, чтобы отделить собственные списки программы от вспомогательных списков для символов Второй аргумент команды glCallLists - указатель на список имен дисплейных списков, если аргумент имеет тип PChar, то мы передаем этим аргументом список кодов нужных символов выводимой строки.

Используемые для вывода символов списки должны по окончании работы приложения удаляться точно так же, как и прочие дисплейные списки:

glDeleteLists (GLF_START_LIST, 256);

Как я уже отмечал, подготовка списков для всех 256 символов - процесс сравнительно длительный, и очень желательно сократить время его выполнения. Для этого можно брать не все символы таблицы, а ограничиться только некоторыми пределами. Например, если будут выводиться только заглавные буквы латинского алфавита, третий параметр команды wgiuseFontOutimes достаточно взять равным 91, чтобы захватить действительно используемые символы.
А теперь посмотрим проект из подкаталога Ех12, пример на анимацию: текст крутится в пространстве, меняя цвет - изменяются свойства материала. На символы текста накладывается одномерная текстура

Замечание
В примерах этого раздела на сцене присутствуют только символы текста При их выводе текущие настройки сцены сбиваются Начинающие очень часто, натыкаясь на эту проблему, оказываются всерьез озадаченными Необходимо перед выводом символов запоминать текущие настройки, вызывая команду glPushAttrib с аргументом GL_ALL_ATTRIB_BITS, а после вывода символов вызывать glPopAttrib

Рассмотрим другой режим вывода текста, основанный на использовании команды wgiuseFontBitmaps Такой способ, пожалуй, редко будет вами применяться, поскольку с успехом может быть заменен на использование текстур.
Посмотрите пример из подкаталога Ех13, отличающийся от первого примера на вывод текста только тем, что команда wgiuseFontOutimes заменена на wgiuseFontBitmaps. Символы в этом случае выводятся аналогично обыкновенному выводу Windows, то есть текст выглядит просто как обычная метка. Как сказано в справке по команде wgiuseFontBitmaps, ею удобно пользоваться, когда вывод средствами GDI в контексте воспроизведения невозможен.
Обратите внимание, что, хотя все связанные с пространственными преобразованиями команды присутствуют в тексте программы, вывод выглядит плоским, а перед выводом директивно задается опорная точка для вывода текста:

glRasterPos2f (0,0);

Как следует из документации, для вывода символов, подготовленных командой wgiuseFontBitmaps, неявно вызывается команда glBitmap. Подобный вывод текста предлагается использовать для подрисуночных подписей, что и проиллюстрировано примером из подкаталога Ех14 (рис 6.6).

Рис. 6.6. Без подписи и не догадаешься, что изображено

Разберем подробнее команду giBitMap Начнем с самого простейшего примера, проекта из подкаталога Ех15. Массив rasters содержит образ буквы F. При воспроизведении задается позиция вывода растра и три раза вызывается команда glBitmap:

glRasterPos2f (20.5, 20.5);
glBitmap (10, 12, 0.0, 0.0, 12.0, 0.0, Srasters);

При каждом вызове glBitmap текущая позиция вывода растра смещается поэтому на экране выводимые буквы не слипаются и не накладываются (рис. 6.7).

Рис. 6.7. Пример на использование команды glBitmap

В этом примере для наглядности растр выводится желтым.

Замечание
Обратите внимание, что вызов команды glRasterPos не только задает позицию вывода растра, но и позволяет использовать цвет для его окрашивания. Без вызова этой команды текущий цвет на выводимый растр не распространяется, он будет выводиться белым.

Следующий пример, проект из подкаталога Ех1б, позволяет понять, каким образом выводятся символы, подготовленные командой wgiUseFontBitmaps. Массив rasters представляет собой битовый образ 95 символов, коды которых располагаются в таблице с 32 по 127 позицию. В примере вручную сделано то, что получается при вызове команды wglUseFontBitmaps. В начале работы приложения вызывается процедура, подготавливающая дисплейные списки, по одному списку для каждого символа:

procedure makeRasterFont;
var
i : GLuint;
begin
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
fontOffset := glGenLists (128); // стартовое смещение имен списков
For i := 32 to 127 do begin
glNewList(i + fontOffset, GL_COMPILE); // список очередного символа
glBitmap(8, 13, 0.0, 2.0, 10.0, 0.0, @rasters [i-32]);
glEndlast;
end;
end;

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

procedure printString(s : String);
begin
glPushAttrib (GL_LIST_BIT);// запомнили стартовое смещение имен списков
glListBase(fontOffset);
glCallLists(Length(s), GL_UNSIGNED_BYTE, PChar(s));
glPopAttrib;
end;

Проект из подкаталога Ex 17 содержит пример вывода монохромного растра, считанного из bmp-файла, с использованием команды glBitmap (рис. 6.8).

Рис. 6.8. В программе из bmp-файла считывается монохромный растр

Растр считываем с помощью самостоятельно написанной функции, возвращающей указатель PText, ссылку на считанные данные. Здесь, к сожалению, не удастся применить стандартные классы Delphi, иначе растр выводится искаженным. Функция чтения растра очень похожа на функцию, использованную нами для чтения файлов при подготовке текстуры, в отличиях можете разобраться самостоятельно.
Собственно вывод содержимого монохромного растра прост:

glBitmap(bmWidth, bmHeight, 0, 0, 0, 0, PText);

Ненулевые параметры здесь - размеры растра и ссылка на него.
Следующий пример, продолжающий тему, - проект из подкаталога Ex18, результат работы которого представлен на рис. 6.9.

Рис. 6.9. Выводимые символы корректно располагаются в пространстве

Здесь текст подготавливается и выводится аналогично предыдущему примеру, однако символы рисуются в пространстве и в цвете. Для этого устанавливается нужный цвет, и точка воспроизведения пикселов задается с помощью трех аргументов функции
glRasterPos3f :
glColor3f (1.0, 1.0, 0.0); // текущий цвет-желтый
glRasterPos3f (-0.4, 0.9, -2.0); // позиция воспроизведения пикселов
glBitmap(bmWidth, bmHeight, 0, 0, 0, 0, PText); // вывод растра

Выводимый текст действительно располагается в пространстве, в чем легко убедиться, меняя размеры окна - треугольники корректно располагаются в объеме относительно выводимого текста.
Изменяя размеры окна, обратите внимание: надпись выводится или вся целиком, либо не выводится совсем, если хотя бы один ее пиксел не помещается на экране.
Завершая данный раздел, надо обязательно назвать еще один способ вывода текста, самый простой: вручную расписать по примитивам списки, соответствующие символам алфавита. Так это сделано в проекте из подкаталога Ex19.

Связь экранных координат с пространственными
При создании приложений типа редакторов и модельеров проблема связи оконных координат с пространственными становится весьма важной Недостаточно выбрать объект или элемент на экране, необходимо соотнести перемещения указателя мыши с перемещением объекта в пространстве или изменением его размеров.
Один из часто задаваемых вопросов звучит так. "Какой точке в пространстве соответствует точка на экране с координатами, например, 100 и 50?"
Ответить на такой вопрос однозначно невозможно, ведь если на экране присутствует проекция области пространства, то под курсором в любой точке окна находится проекция бесконечного числа точек пространства Однако ответ станет более определенным, если имеется в виду, что под курсором не пусто, есть объект. В этом случае, конечно, содержимому соответствующего пиксела соответствует лишь одна точка в пространстве.
Для решения несложных задач по переводу координат можно воспользоваться готовой командой библиотеки glu gluUnProject, переводящей оконные координаты в пространственные координаты Пример из подкаталога Ex20 поможет нам разобраться с этой командой В нем рисуется куб, при щелчке кнопки выводится информация о соответствующих мировых координатах (рис. 6.10).

Рис. 6.10. Для простейших задач перевода координат может использоваться gluUnProject

Обработка щелчка выглядит следующим образом:

procedure TfrmGL.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Viewport : Array [0..3] of GLInt; // область вывода
mvMatrix, // матрица модели
ProjMatrix : Array f0..15] of GLDouble; // матрица проекций
RealY : GLint; // OpenGL у - координата
wx, wy, wz : GLdouble; // возвращаемые мировые x, у, z координаты
Zval : GLfloat; // оконная z - координата
Begin
glGetIntegerv (GL_VIEWPORT, @Viewport); // матрица области вывода
// заполняем массивы матриц
glGetDoublev (GL_MODELVIEW_MATRIX, @mvMatrix);
glGetDoublev (GL_PROJECTION_MATRIX, @ProjMatrix) ; // viewport[3] - высота окна в пикселах, соответствует Height
RealY := viewport[3] - Y - 1;
Caption := 'Координаты курсора ' + IntToStr (x) + ' ' +
FloatToStr (RealY);
glReadPixels(X, RealY, 1, I, GL_DEPTH_COMPONENT, GL_FLOAT, @Zval);
gluUnProject (X, RealY, Zval,
@mvMatrix, @ProjMatrix, @Viewport, wx, wy, wz) ;
ShowMessage ('Мировые координаты для z=' + FloatToStr(Zval)
+ ' : ' + chr (13) + '(' + FloatToStr(wx)
+ '; ' + FloatToStr(wy) + '; '
+ FloatToStr(wz) + ')');
end;

Команда gluUnProject требует задания трех оконных координат - X, Y, Z Третья координата здесь - значение буфера глубины соответствующего пиксела, для получения которого пользуемся командой glReadPixels, указав в качестве аргументов координаты пиксела и задав размер области 1x1.
Обратите внимание, что при нажатии кнопки на пустом месте значение буфера глубины максимально и равно единице, для точек, наименее удаленных от точки зрения, значение буфера глубины минимальное
Команда gluUnProject имеет парную команду - gluProject, переводящую координаты объекта в оконные координаты Для ознакомления с этой командой предназначен проект из подкаталога Ех21 Действие приложения заключается в том, что в пространстве по кругу перемещается точка, а ее оконные координаты непрерывно выводятся в компоненте класса TMemo в правой части экрана (рис 6.11).
Для проверки достоверности результатов в заголовке окна выводятся координаты курсора.

Рис. 6.11. Команда gluProject позволяет узнать, в какой точке экрана осуществляется воспроизведение

Программа важная, поэтому разберем ее поподробнее С течением времен изменяется значение угла поворота по оси X, увеличивается значение переменной Angle При перерисовке окна происходит поворот системы координат на этот угол и воспроизведение точки с координатами (0, 0, - 0 5)

glRotatef (angle,1, 0, 0.1); // поворот системы координат
glColor3f (1, 1, 0); // текущий цвет
glBegin (GL_POINTS); // воспроизведение точки в пространстве
glNormal3f (О, О, -1);
glVertex3f (О, О, -0.5);
glEnd;
Print; // обращение к процедуре вывода оконных координат

Процедура вывода оконных координат выглядит так:

procedure TfrmGL.Print;
var
Viewport : Array [0..3] of GLint;
mvMatrix, ProjMatrix : Array [0..15] of GLdouble;
wx, wy, wz : GLdouble; // оконные координаты
begin
// заполнение массивов матриц
glGetlntegerv (GL_VIEWPORT, @Viewport);
glGetDoublev (GL_MODELVIEW_MATRIX, @mvMatrix),
glGetDoublev (GL_PROJECTION_MATRIX, @ProjMatrix);
// перевод координат объекта в оконные координаты
gluProject (0, 0, -0.5, @mvMatrix, @ProjMatrix, @Viewport, wx, wy, wz);
// собственно вывод полученных оконных координат
Memol.Clear; Memol.Lines.Add('') ,
Memol.Lines.Add('Оконные координаты.');
Memol.Lines.Add(' x = ' + FloatToStr (wx) ) ;
Memol.Lines.Add(' у = ' + FloatToStr (ClientHeight - wy) ) ;
Memol.Lines.Add(' z = ' + FloatToStr (wz));
end;

Первые три аргумента команды gluProject - мировые координаты точки, следующие три аргумента - массивы с характеристиками области вывода и матриц, последние три аргумента - возвращаемые оконные координаты
Учтите, что по оси Y возвращаемую координату требуется преобразовать к обычной оконной системе координат, здесь я вывожу значение выражения (ClientHeight - wy). Следя указателем курсора за точкой, легко убедиться в полном соответствии результатов

Режим обратной связи
Библиотека OpenGL предоставляет еще один механизм, облегчающий построение интерактивных приложений - режим воспроизведения FeedBack, обратной связи. При этом режиме OpenGL перед воспроизведением каждой очередной вершины возвращает информацию о ее оконных координатах и цветовых характеристиках.
Обратимся к примеру, проекту из подкаталога Ех22, мгновенный снимок работы которого показан на рис. 6.12.

Рис. 6.12. В режиме обратной связи библиотека OpenGL уведомляет о всех своих действиях

В пространстве крутятся площадка и точка над ней, в компоненте класса TMemo выводится информация о каждой воспроизводимой вершине. Сразу же обращаем внимание, что говорится о воспроизведении двух многоугольников по трем вершинам и одной отдельной вершины - точки, или примитиву типа GL_POINTS. Многоугольники соответствуют воспроизводимому в программе примитиву типа GL_QUADS (в главе 2 мы говорили о том, что каждый многоугольник при построении разбивается на треугольники).
В программе введен тип для операций с массивом буфера обратной связи:

type
TFBBuffer = Array [0..1023] of GLFloat;

При начале работы приложения сообщаем системе OpenGL, что в качестве буфера обратной связи будет использоваться переменная fb описанного выше типа:

glFeedbackBuffer(SizeOf (fb), GL_3D_COLOR, @fb);

Константа, стоящая на месте второго аргумента, задает, что для каждой вершины будет возвращаться информация о трех координатах и четырех цветовых составляющих. Полный список констант вы найдете в файле справки.
Собственно построение оформлено в виде отдельной процедуры, обращение к которой происходит с аргументом, принимающим значения GL_RENDER или GL_FEEDBACK. Если аргумент равен второму возможному значению, то перед воспроизведением каждого примитива в буфер обратной связи вызовом команды glPassThrough помещается маркер:

procedure Render (mode: GLenum);
begin
If mode = GL_FEEDBACK then glPassThrough(1); // помещаем маркер - 1
glColor3f (1.0, 0.0, 0.0);
glNormalSf (0.0, 0.0, -1.0);
glBegin (GL_QUADS);
glVertexSf (-0.5, -0.5, 0.0);
glVertex3f (0.5, -0.5, 0.0);
glVertexSf (0.5, 0.5, 0.0);
glVertex3f (-0.5, 0.5, 0.0);
glEnd;
If mode = GL_FEEDBACK then glPassThrough(2); // помещаем маркер - 2
glColorSf (1.0, 1.0, 0.0); glBegin (GL_POINTS);
glNormalSf (0.0, 0.0, -1.0);
glVertex3f (0.0, 0.0, -0.5);
glEnd;
end;

При перерисовке экрана процедура Render вызывается с аргументом GL_RENDER, для собственно воспроизведения. Затем режим воспроизведения задается режимом обратной связи, и снова происходит воспроизведение, но в установленном режиме оно не влияет на содержимое буфера кадра:

glRenderMode(GL_FEEDBACK); Render(GL_FEEDBACK) ;

При последующем переключении в обычный режим воспроизведения команда glRenderMode возвращает количество числовых значений, помещенных в буфере обратной связи. Как подчеркивается в файле справки, это не число вершин. Полученную величину передаем в пользовательскую процедуру, заполняющую Memo1:

n := glRenderMode(GL_RENDER);
If n > 0 then PrintBuffer(fb, n) ;

Процедура вывода содержимого буфера обратной связи выглядит так:

procedure TfrmGL.PrintBuffer(b: TFBBuffer; n: Integer);
var
i, j, k, vcount : Integer;
token : Single;
vert : String;
begin
Memol.Clear;// очищаем содержимое
Memo i := n;
While i <> 0 do begin // цикл анализа содержимого буфера
token := b[n-i]; // тип последующих данных
DEC(i) ;
If token = GL_PASS__THROUGH_TOKEN then begin
// маркер Memol.Lines.Add('');
Memol.Lines.Add(Format('Passthrough: %.2f, [b[n-i]])); DEC(i) ;
end
else If token = GL_POLYGON_TOKEN then begin // полигон
vcount := Round(b[n-i]); // количество вершин полигона
Memol.Lines.Add(Format('Polygon - %d vertices (XYZ RGBA):',
[vcount]));
DEC(i);
For k := 1 to vcount do begin // анализ вершин полигона
vert := ' ';
// для типа GL_3D_COLOR возвращается 7 чисел (XYZ and RGBA).
For j := 0 to 6 do begin
vert := vert + Format('%4.2f ', [b[n-i]]); DEC(i) ;
end;
Memol.Lines.Add(vert);
end;
end
else If token = GL_POINT_TOKEN then begin // точки
Memol.Lines.Add('Vertex - (XYZ RGBA):');
vert := ' '; For j := 0 to 6 do begin
vert := vert + Format('%4.2f ', [b[n-i]]); DEC(i);
end;
Memol.Lines.Add(vert);
end;
end;
end;

Из комментариев, надеюсь, становится ясно, как анализировать содержимое буфера обратной связи.
Для сокращения кода я реализовал анализ только для двух типов - точек и полигонов. В документации по команде glFeedbackBuffer вы найдете описание всех остальных типов, используемых в этом буфере.
Чтобы легче было разбираться, я предусмотрел остановку движения объектов по нажатию клавиши пробела и вывод координат курсора в заголовке окна. Обратите внимание на две вещи - на то, что оконная координата вершин по оси Y выводится без преобразований и на то, что координаты по осям выводятся через пробел. Запятая здесь может сбить с толку, если в системе установлен именно такой разделитель дробной части.
Механизм обратной связи легко приспособить для выбора объектов. Имея необходимые оконные координаты, например координаты курсора, легко выяснить по меткам объектов, какие вершины лежат вблизи этой точки
Подкрепим это утверждение примером. Проект из подкаталога Ех23 представляет собой модификацию примера на выбор, где рисовались треугольники в случайных точках экрана и со случайным цветом, при нажатии кнопки мыши треугольник под курсором перекрашивался. Сейчас при нажатии кнопки перекрашиваются все треугольники, находящиеся вблизи курсора. Массив буфера достаточно ограничить сотней элементов:

FBBuf : Array [0..100] of GLFloat;

При создании окна создаем буфер обратной связи, сообщая OpenGL, что нам достаточно знать только положение вершины в окне:

glFeedbackBuffer(SizeOf (FBBuf), GL_2D, @FBBuf);

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

procedure Render (mode : GLenum); // параметр - режим (выбора/рисования)
var
i : GLuint; begin
For i := 0 to МАХОВJS - 1 do begin
// загрузка очередного имени - метка в буфере обратной связи
If mode = GL_FEEDBACK then glPassThrough (i) ;
glColor3fv(@objects[i].color); // цвет для очередного объекта
glBegin(GL_POLYGON); // рисуем треугольник
g!Vertex2fv(@objects[i].v1) ;
glVertex2fv(@ob]ects[i].v2);
glVertex2fv(@objects[i] .v3) ;
glEnd;
end;
end;

При каждой перерисовке окна картинка воспроизводится дважды: первый раз в буфер кадра, второй раз - в буфер обратной связи:

Render(GL_RENDER); // рисуем массив объектов без выбора
glRenderMode(GL_FEEDBACK); // режим обратной связи
Render (GL_FEEDBACK); // воспроизведение в режиме обратной связи
n := glRenderMode(GL_RENDER); // передано чисел в буфер обратной связи

Функция выбора из первоначального проекта переписана в процедуру, перекрашивающую треугольники в районе точки, координаты которой передаются при ее вызове. Теперь при щелчке кнопки мыши вызывается эта процедура, и окно перерисовывается:

procedure DoSelect{x : GLint; у : GLint);
var
i, k : GLint; token : GLFloat;
vcount, w, nx, ny : Integer;
begin i := n;
While i <> 0 do begin // цикл анализа содержимого буфера
token := FBBuf[n-i]; // признак
DEC(i) ;
If token = GL_PASS_THROUGH_TOKEN then begin // метка
w := round(FBBUF [n-i]); // запомнили номер треугольника
DEC(i) ;
end
else If token = GL_POLYGON_TOKEN then begin
vcount := Round(FBBUF[n-i]); // количество вершин полигона
DEC(i);
For k := 1 to vcount do begin
nx := round (FBBUF[n-i]}; // оконная координата х вершины
DEC(i);
ny := windH - round (FBBUF[n-i]); // оконная координата у вершины
DEC(i);
// одна из вершин треугольника находится вблизи курсора,
// т. е. внутри круга радиусом 30
If (nx + 30 > х) and (nx - 30 < x) and (ny + 30 > у) and (ny - 30 <
  y) then
RecolorTri (w); // перекрашиваем треугольник
end;
end;
end;
end;

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

Трансформация объектов
В предыдущих разделах мы разобрали несколько способов выбора объектов и поговорили о соотношении экранных координат с пространственными координатами. Попробуем совместить полученные знания, чтобы интерактивно трансформировать экранные объекты, как это делается в графических редакторах и модельерах. Проект из подкаталога Ех24 содержит пример простейшего редактора, я его назвал редактор сетки. При запуске на экране появляется двумерная сетка с выделенными опорными точками, имитирующая проекцию трехмерной поверхности (рис. 6.13).

Рис. 6.13. Простейший интерактивный графический редактор

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

Рис. 6.14. Картинки плоскостные, но выглядят как пространственные

При выводе контрольных точек в режиме выбора точки последовательно именуются:

For i := 0 to 3 do
For j := 0 to 3 do begin
If mode = GL_SELECT then glLoadName (i * 4 + 3); // загрузка имени
glBegin (GL_POINTS);
glVertexSfv (@grid4x4[i] [j ] ) ; // вершины массива опорных точек
glEnd;
end;
При щелчке кнопки находим номер опорной точки под курсором
selectedPoint := DoSelect (x, у); // выбранная точка

При выборе воспроизводятся только опорные точки, саму сетку воспроизводить нет смысла Перемещая курсор, вызываем команду gluUnProject для получения соответствующих пространственных координат

If selectedPoint >= 0 then Begin
// получаем пространственные координаты, соответствующие
// положению курсора
gluUnProject(x, ClientHeight - у, 0.95, @modelMatrix, @projMatrix,
@viewport, objx, objy, objz);
// передвигаем выбранную точку
grid4x4 [selectedPoint div 4, selectedPoint mod 4, 0] := objx,
grid4X4 [selectedPoint div 4, selectedPoint mod 4, 1]:= objy,
InvalidateRect(Handle, nil, False);
end

Используемые массивы с характеристиками видовых матриц заполняются при обработке события onResize
Этот пример может оказаться полезным для построения несложных редакторов Положение соответствующей точки в пространстве здесь находится точно, независимо от текущих установок видовых параметров, опорные точки перемещаются вслед за курсором Хотя получаемые построения выглядят вполне "объемно", здесь мы имеем дело с двумерными изображениями При переходе в пространство, несмотря на легкость перевода экранных координат в координаты модели, избежать неоднозначности определения положения точки в пространстве не удается При изометрической проекции объемных объектов нелегко понять замысел пользователя, перемещающего элемент в пространстве Многие редакторы и модельеры имеют интерфейс,
подобный интерфейсу рассмотренного примера, когда элементы перемещаются в точности вслед за курсором, однако лично я часто становлюсь в тупик, работая с такими приложениями При проектировании нашего собственного редактора мы предложим несколько иной подход, избавляющий от этой неопределенности.

Постановка задачи
Сейчас мы полностью подготовлены к тому, чтобы рассмотреть проект из подкаталога Ex25, представляющий собой пример графического редактора или построителя моделей, модельера Практическая ценность полученной программы будет, возможно, небольшой, однако она вполне может стать шаблоном для создания более мощной системы Многие разработчики работают над модулями ввода данных и визуализации для различных CAD-систем, и создаваемый редактор, думаю, будет им очень полезен Но главные цели, которые мы здесь преследуем, носят все же скорее учебный, чем практический характер
Пространство модели представляет собой опорную площадку и оси координат (рис 6.15).

Рис. 6.15. Пространство модели нашего собственного модельера

Оси координат красиво подписаны объемными буквами, а по трем плоскостям нанесена разметка, которую будем называть сеткой Размер сетки ограничивается длиной осевых линий. Площадка, как и все остальные элементы, строится только для удобства ориентирования в пространстве, объекты могут располагаться где угодно, без каких-либо ограничений.
В правой части экрана располагается панель, содержащая элементы управления: компоненты класса TUpDown, позволяющие устанавливать точку зрения в любое место в пространстве, а также компонент того же класса, позволяющий менять длину осевых линий.
Кроме того, на панели располагаются кнопки, отвечающие за чтение/запись проектируемой модели, а также кнопка "Освежить" - перерисовка экрана. Внизу панели помещены несколько компонентов класса TCheckВох, задающих режимы рисования: наличие осей, сетки, тумана и площадки.
Так, по нажатию на кнопки элемента с надписью "Расстояние" можно приближать или удалять точку зрения к пространству модели, элементы "Сдвиг" позволяют передвигать точку зрения по соответствующей оси, а элементы "Угол" - поворачивать модель в пространстве относительно указанной оси Пара замечаний по поводу этих действий. Перспектива задается с помощью Команды glFrustum:

glFrustum (vLeft, vRight, vBottom, vTop, zNear, zFar);

При нажатии на кнопки элемента "Расстояние" (компонент назван udDistance) изменяются значения параметров перспективы в зависимости от того, какая нажата кнопка, нижняя или верхняя:

If udDistance.Position < Perspective then begin
vLeft := vLeft + 0.025;
vRight := vRight - 0.025;
vTop := vTop - 0.025;
vBottom := vBottom + 0.025;
end
else If udDistance.Position > Perspective then begin
vLeft := vLeft - 0.025;
vRight := vRight + 0.025;
vTop := vTop + 0.025;
vBottom := vBottom - 0.025;
end;
Perspective := udDistance.Position;

Переменная Perspective - вспомогательная и хранит значение свойства Position объекта.
После изменения установок проекции экран перерисовывается. Здесь я неожиданно столкнулся с небольшой проблемой, связанной с тем, что стандартный компонент класса TCheckBox работает не совсем корректно. Вы можете заметить, что первое нажатие на кнопку срабатывает так, как будто была нажата кнопка с противоположным действием Я потратил много времени на устранение этой ошибки, однако избавиться от нее так и не смог, по-видимому, ошибка кроется в самой операционной системе Решение я нашел в том, что самостоятельно описал все действия по обработке событий, связанных с компонентом: анализ положения курсора в пределах компонента, т. е. на какой кнопке он находится, включение таймер, по тику которого производятся соответствующие действия, и выключение его, когда курсор уходит с компонента Все эти действия проделываются в элементах, связанных с поворотом и сдвигом пространства модели, и вы можете сравнить соответствующие фрагменты кода. Конечно, программа стала громоздкой и менее читабельной, однако более простого решения проблемы найти не получилось.
Использование элементов управления для перемещения точки зрения наблюдателя является обычным для подобных приложений подходом, однако необходимо продублировать эти действия управлением с клавиатуры. Поэтому предусмотрена обработка нажатий клавиш 'X', 'Y' и 'Z' для поворотов пространства моделей по осям, а комбинация этих клавиш с <Alt> аналогична нажатию элементов сдвига по нужной оси. Если при этом удерживать еще и <Shift>, то соответствующие величины сдвига или поворота уменьшаются. Также введена клавиша, по которой можно быстро переместиться в привычную точку зрения (я назвал ее изометрической).
Шаг изменения величин поворота и сдвига можно менять, для чего соответствующие элементы управления имеют всплывающие меню, при выборе пунктов которых появляются дочерние окна (рис. 6.16).

Рис. 6.16. Пользователю привычнее работать с такими окнами, чем с клавишами клавиатуры

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

procedure TfrmMain.ScreenToSpace (mouseX, mouseY : GLInt; var X, Y :
GLFloat);
var
xO, xW, yO, yH : GLFloat;
begin
xO := 4 * zFar * vLeft / (zFar + zNear); // 0
xW := 4 * zFar * vRight / (zFar + zNear); // Width
yO := 4 * zFar * vTop / (zFar + zNear); // 0
yH := 4 * zFar * vBottom / (zFar + zNear); // Heigth
X := xO + mouseX * (xW - xO) / (ClientWidth - Panell.Width);
Y := yO + mouseY * (yH - yO) / ClientHeight;
end;

Перевод здесь условный, в результате него получаются только две пространственные координаты из трех. Первые два аргумента процедуры - экранные координаты. Для перевода в пространственные координаты используется то,
что перспектива задается с помощью команды glFrustum. Координаты крайних точек окна выражаются через координаты плоскостей отсечения, передаваемые экранные координаты приводятся к этому интервалу. При изменении размеров площадки с помощью этой процедуры получаются условные пространственные координаты для предыдущего и текущего положений курсора. Приращение этих координат дает величину, на которую пользователь сместился в пространстве. Но это условная величина, и при переводе в реальное пространство требуется ее корректировка (в рассматриваемой программе при переводе она умножается на 5). В результате не получается, конечно, совершенно точного соответствия, но погрешность здесь вполне терпимая, и пользователь, в принципе, не должен испытывать сильных неудобств. Что касается изменения размеров площадки, то ее длина умножается на 10, т. е. приращение удваивается, чтобы собственно координаты вершин смещались с множителем 5. При изменении размеров и положения объектов модели два этих множителя комбинируются, чтобы получить удовлетворительную чувствительность.
Если же требуется точное соответствие с позицией курсора, можно опираться на значение вспомогательной переменной Perspective, косвенно связанной с текущими видовыми параметрами.
При трансформациях объектов в изометрической проекции для устранения неопределенности с положением точки в пространстве приращение по каждой экранной оси по отдельности переводится в пространственные координаты и трактуется в контексте объемных преобразований. То есть, если пользователь передвигает курсор вверх, уменьшается координата Y объекта, если курсор идет вправо, увеличивается координату X . Объект "уплывает" из-под курсора, однако у наблюдателя не возникает вопросов по поводу третьей координаты. Если же удерживается клавиша <Ctrl>, то изменение экранной координаты Y указателя берется для трансформации по оси Z мировой системы координат. Конечно, это не идеальный интерфейс, но я нахожу его вполне удовлетворительным. Иначе придется заставлять пользователя работать только в плоскостных проекциях, наблюдая постоянно модель со стороны какой-либо из осей. Для хранения данных об объекте модели введен тип:

TGLObject = record
Kind : (Cube, Sphere, Cylinder); // тип объекта
X, Y, Z, // координаты в пространстве
L, W, H : GLDouble; // длина, ширина, высота
RotX, RotY, RotZ : GLDouble; // углы поворота по осям
Color : Array [0..2] of GLFloat; // цвет
end;

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

For i := 1 to objectcount do begin
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, @objects[i] .color) ;:
glPushMatrix;
glTranslatef (objects[i].X, objects[i].Y, objects[i].Z);
glScalef (objects[i].L, objects[i].W, objects[i].H);
glRotatef (objects[i].RotX, 1.0, 0.0, 0.0);
glRotatef (objects[i].RotY, 0.0, 1.0, 0.0);
glRotatef (objects[i].RotZ, 0.0, 0.0, 1.0);
If mode = GL_SELECT then glLoadName (i + startObjects);
case objects [i].Kind of
Cube : glCallList (DrawCube);
Sphere : glCallList (DrawSphere);
Cylinder : glCallList (DrawCylinder);
end;
{case}
If i = MarkerObject then MarkerCube (i, mode);
glPopMatrix;
end;

Один из объектов может быть помеченным, маркированным. Такой объект выделяется среди прочих тем, что объем, занимаемый им, размечен восемью маленькими кубиками, маркерами (рис. 6.17).

Рис. 6.17. Маркированный объект можно трансформировать

При прохождении курсора над маркерами он меняет вид, и при нажатии кнопки мыши можно визуально менять размеры объекта. Помеченный объект можно также перемещать указателем так, как мы обсудили выше. Элементы управления панели при наличии маркированного объекта действуют только на него, т. е. нажатие на кнопку уменьшения угла по оси X приведет к повороту не всей модели, а только маркированного объекта. Для точного задания значений параметров маркированного объекта предусмотрен вывод по нажатию клавиши <Enter> дочернего окна "Параметры объекта".

Структура программы
Думаю, нет необходимости рассматривать подробно всю программу модельера, я постарался сделать код читабельным, а ключевые моменты поясняются комментариями.
Однако несколько вещей требуют дополнительного рассмотрения.
Пользователь должен иметь возможность отмены ошибочных действий. Вести протокол всех манипуляций накладно, поэтому я ограничился только возможностью отмены одного последнего действия. Перед выполнением операции редактирования система объектов копируется во вспомогательный массив, а отмена последнего действия пользователя заключается в процедуре обратного копирования.
Систему в любой момент можно записать в файл одного из двух типов. Файлы первой группы (я их назвал "Файлы системы") имеют тип TGLObject, это собственный формат модельера. Файлы второй группы имеют расширение Inc, это текстовые файлы, представляющие собой готовые куски программы, пригодные для включения в шаблоны программ Delphi. Посмотрите пример содержимого такого файла для единственного объекта:

glPushMatrix;
glTranslatef (11.11,10.35,10.03);
glScalef ( 1.00, 2.00, 3.00);
color [0] := 0.967;
color [1] := 0.873;
color [2] := 0.533;
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, @color);
glCallList (DrawCube)
glPopMatrix;

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

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

  • При компиляции в Delphi пятой версии появляется совершенно неожиданная ошибка с отображением осевых линий, длина которых после запуска почему-то обнуляется.
  • Для подобных систем общепринято, чтобы координатные оси раскрашивались различными цветами: ось X - красным, ось Y - зеленым, ось Z - синим. Понятно, что это облегчает ориентирование в пространстве.
  • Можно дополнить интерфейс возможностью навигации в пространстве с использованием мыши, если нет помеченных объектов.
  • Можно включить возможность изменения положения источника света. Достаточно его визуализировать и перемещать по тем же принципам, что и объекты модели.
  • Объектам модели можно добавить свойство невидимости, чтобы на время скрывать некоторые их них, дабы не загромождать экран.
  • Необходимо добавить масштабирование всей модели, иначе для больших объектов изображение получается чересчур искаженным.
  • Я разделил запись параметров системы и саму модель по различным файлам. Некоторые параметры, такие как положение точки зрения и масштаб модели, удобнее записывать в файл системы.
  • Система хранится в массиве, следовательно, имеет ограничение по количеству объектов. Использовать список для устранения этого ограничения.
  • Можно добавить возможность наложения текстуры на объекты. В этом случае нужно решить, записывать ли с моделью образы текстур объектов или для экономии дискового пространства достаточно ограничиться записью имен файлов текстур.
  • Желательно реализовать стандартные операции по работе с буфером обмена Windows. Самое простое решение - эмуляция этих операций с использованием еще одного массива, подобного массиву для отмены последнего действия пользователя.
  • Полезной будет возможность объединения нескольких объектов в набор. Операции по перемещению, копированию и удалению распространить в этом случае на все объекты набора.
  • Конечно, тени, прозрачность и зеркальность объектов придадут модельеру совершенно новый облик.
  • Необходима возможность записи модели в открытых форматах, таких как DXF, WRL, SPT, POV. Также можно предусмотреть возможность встраивания модулей других разработчиков для записи в любом формате.
  • Переход на использование патчей потребует переделки программной структуры модельера, но я верю, что решение этой задачи вполне вам по силам.

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


OpenGL в Delphi - Заключение

Заключение

Несмотря на сравнительно небольшой объем, эту книгу вполне можно считать полным руководством по использованию библиотеки OpenGL. В частности, для режима RGBA не оставлено без внимания ни одной команды. Поэтому я уверен, что даже опытные программисты, работающие с OpenGL, найдут здесь для себя много полезного. Однако главный замысел книги состоял все же в том, чтобы предоставить начинающему пользователю информацию по OpenGL достаточную для того, чтобы легко и быстро научиться применять эту библиотеку в Delphi.
Мне, автору, трудно судить о том, удался ли мой замысел, поэтому я очень заинтересован в том, чтобы узнать мнение читателей об этой книге.
После того как книга была написана, у меня родилась идея написать ее продолжение. Оно может касаться использования DirectX в Delphi или содержать расширенный набор примеров по созданию визуальных эффектов с помощью OpenGL. Либо это может быть подробное руководство по всем темам использования графики в Delphi. Возможно, читатели будут заинтересованы в учебнике по Kylix - готовящейся к выпуску среде программирования для Linux.
Вы можете высказать свои замечания и пожелания по адресу softgl@chat.ru. Вы также можете обращаться ко мне с вопросами по поводу использования OpenGL, я постараюсь обязательно помочь. Я отвечаю на все письма. Создание книги - большой труд, который компенсируется за счет ее продажи. Если вам понравилась эта книга, а приводимые в ней примеры оказались для вас полезными, пожалуйста, не распространяйте их в качестве свободных программ. Вы, конечно, можете использовать примеры в собственных целях, иначе какой же в них прок, но при распространении ваших модулей оставляйте оригинальный копирайт. Так же поступил и я с теми модулями, которые взял у других авторов для построения некоторых примеров - все первоначальные источники указаны в тексте книги и текстах модулей. В заключение хочу выразить искреннюю благодарность фирме Microsoft за право использовать в книге содержимое файла справки opengl.hlp, Марку Килгарду (Mark Kilgard) - за предоставленное им право использовать, конвертировать и копировать исходные модули библиотеки glut и корпорации SGI за право использования исходных модулей программ.


OpenGL в Delphi - ПРИЛОЖЕНИЕ 1 OpenGL в Интернете

ПРИЛОЖЕНИЕ 1 OpenGL в Интернете

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

  • http://www.opengl.org
    С этого сайта необходимо начинать знакомство с библиотекой OpenGL.
  • http://www.torry.ru/samples/saniples/primscr.zip
    Пример использования OpenGL в Delphi, из этого источника можно взять модуль DGLUT.pas.
  • http://www.torry.ru/vcl/mmedia/ogl.zip Редактор на основе компонента TOpenGL. Автор - Enzo Piombo:
  • http://www.geocities.com/SiliconValley/Hills/6131
  • http://www.torry.ru/vcl/mmedia/ogld lO.zip Заголовочные файлы gl.pas и glu.pas. Автор - Alexander Staubo: http ://home .powertech. no/alex/
  • www.lischke-online.de
    Сайт Mike Lischke, содержит Opener, программу просмотра (viewer) 3DS-файлов, а также пакет GLScene.
  • http://www.delphi-jedi.org/DelphiGraphics/OpenGL/OpenGL.zip
    Альтернативный заголовочный файл opengl.pas. Автор - Mike Lischke. 336
  • www.gamedeveloper.org/delphi3d
    Сайт Tom Nuydens, содержит пакет CgLib и массу примеров и документации на его основе. Здесь можно получить заголовочный файл для использования библиотеки GLUT.
  • http://www.scitechsoft.com
    Библиотека программирования графики SciTech MGL.
  • http://wwwl.math.luc.edu/~jlayous/opengl/index.html http://www.p-m.org/delphi/ http://users.cybercity.dk/~bbl6194/delphi3dx.htm
  • http://www.geocities.com/SiliconValley^Vay/2132/
    Личные Web-страницы разработчиков, использующих OpenGL в проектах Delphi.
  • http://www.signsoft.com/downloadcenter/index.html Набор компонентов VisIt.
  • http://gl.satel.ru/
    Сайт "OpenGL в России".
    Ссылки на сайты и российские конференции.
  • http://www.sgl.com/software/opengl
    Курсы программирования для OpenGL.
    На этом сайте вы можете получить альтернативную версию OpenGL. П http://propro.ru/go/gallery.html
    Страница Геннадия Обухова, предоставившего модели для примеров этой книги.
  • http://delphi.vitpc.com
    Великолепный сайт "Королевство Delphi", на котором, в частности, находится раздел с дополнительными примерами по использованию OpenGL.


OpenGL в Delphi - ПРИЛОЖЕНИЕ 2 Список литературы

ПРИЛОЖЕНИЕ 2 Список литературы

1. Тихомиров Ю. В. Программирование трехмерной графики в Visual C++. - СПб.: BHV - Санкт-Петербург, 1998. - 256 c.: ил.
2. Майкл Янг. Программирование графики в Windows 95: Векторная графика на языке С++/Пер. с англ. - M.: Восточная книжная компания, 1997. - 368 c.: ил.
3. Шикин А. В., Боресков А. В. Компьютерная графика. Динамика, реалистические изображения. - M.: ДИАЛОГ-МИФИ, 1998. - 288 с.