к оглавлению

Создаем свой редактор

Выбор элементов
Буфер выбора
Вывод текста
Связь экранных координат с пространственными
Режим обратной связи
Трансформация объектов

Постановка задачи
Структура программы
Несколько советов

Эта глава посвящена разработке графического редактора на основе 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], 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); o // шпильковерт со шпинделем
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 хранит имена в стеке имен.
Для включения режима выбора необходимо вызвать команду giRenderMode с аргументом GL_SELECTION. Однако прежде чем сделать это, требуется определить буфер вывода, куда будут помещаться записи нажатия. При нахождении в режиме выбора содержимое заднего буфера кадра закрыто и не может быть изменено.
Библиотека OpenGL будет возвращать запись нажатия для каждого объекта, находящегося в отображаемом объеме. Для выбора только среди объектов, находящихся под курсором, необходимо изменить отображаемый объем. Библиотека glu содержит команду, позволяющую это сделать - giuPickMatrix, которая создает небольшой отображаемый объем около координат курсора, передаваемых в команду в качестве параметров. После задания области вывода можно только выбирать объекты. Напоминаю, что перед рисованием. Объектов вызываются команды glLoadName ИЛИ glPushName.
После осуществления выбора необходимо выйти из режима выбора вызовом команды giRenderMode с аргументом GL_RENDER. С этого момента команда будет возвращать число записей нажатия, и буфер выбора может быть проанализирован. Буфер выбора представляет собой массив, где каждая запись нажатия содержит следующие элементы:

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

Однако есть и недостатки:

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

procedure Render (mode : GLenum); // параметр - режим (выбора/рисования)
begin
// красный треугольник
If mode = GL_SELECT then glLoadNarae (1); // называем именем 1
glColor3f (1.0, 0.0, 0.0);

Процедурой Render будем пользоваться для собственно построения изображения и для выбора объекта, режим передается в качестве параметра В случае, если действует режим выбора, перед рисованием объекта загружаем его
имя командой glLoadName, аргумент которой - целое число, задаваемое разработчиком. В примере для первого, красного треугольника, загружаем единицу, для второго треугольника загружаем двойку.
При нажатии кнопки мыши координаты курсора передаются в функцию DoSeiect. Это важная часть программы, поэтому приведем описание функции целиком.

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;

Функция начинается с включения режима выбора. Команда glimtNames очищает стек имен, команда glPushName помещает аргумент в стек имен. Значение вершины стека заменяется потом на аргумент команды glLoadName.
Команда gluPickMatrix задает область выбора. Первые два аргумента - центр области выбора, в них передаем координаты курсора. Следующие два аргумента задают размер области в пикселах, здесь задаем размер области 2x2. Последний аргумент - указатель на массив, хранящий текущую матрицу. Ее запоминаем в обработчике события WM_sizE при каждом изменении размеров окна:

glGetIntegerv(GL_yiEWPORT, @vp);

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

glSelectBuffer{MaxSelect, @SelectBuf); // создание буфера выбора

Первый аргумент - размер буфера выбора, в примере задан равным 4 - ровно под один объект, поскольку в проекте выбирается и перекрашивается один, самый верхний (ближайший к наблюдателю) объект.
Более элегантный код для этого действия записывается так:

glSelectBuffer(SizeOf(SelectBuf), @SelectBuf);

Выражение для извлечения имени элемента ((hits - 1> * 4 + з) в случае, если нам нужно получить имя только последнего, верхнего объекта, можно заменить на просто 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;

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

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. Все объекты, располагающиеся под курсором, доступны для выбора, в том числе

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

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 Cyr1);
hFontNew := CreateFontlndxrect(If);
hOldFont := SelectObuect(DC,hFontNew);
wglUseFontOutlines(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 задает базовое смещение для вызываемых списков, а команда gicaiiLists вызывает на выполнение списки, соответствующие выводимому тексту.

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

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

glDeleteLists (GLF_START_LIST, 256);

Как я уже отмечал, подготовка списков для всех 256 символов - процесс сравнительно длительный, и очень желательно сократить время его выполнения. Для этого можно брать не все символы таблицы, а ограничиться только некоторыми пределами. Например, если будут выводиться только заглавные буквы латинского алфавита, третий параметр команды wgiuseFontOutimes достаточно взять равным 91, чтобы захватить действительно используемые символы.
А теперь посмотрим проект из подкаталога Ех12, пример на анимацию: текст крутится в пространстве, меняя цвет - изменяются свойства материала. На символы текста накладывается одномерная текстура

Замечание
В примерах этого раздела на сцене присутствуют только символы текста При их выводе текущие настройки сцены сбиваются Начинающие очень часто, натыкаясь на эту проблему, оказываются всерьез озадаченными Необходимо перед выводом символов запоминать текущие настройки, вызывая команду glPushAttnb с аргументом 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б, позволяет понять, каким образом выводятся символы, подготовлЕнныЕ комАндой wglUseFontBitmaps. Массив 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. Для простейших задач перевода координат может использоваться gluUnPro]ect

Обработка щелчка выглядит следующим образом:

procedure TfrmGL.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Viewport : Array [0..3] of GLInt; // область вывода
mvMatrix, // матрица модели
Pro]Matrix : 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, @Pro]Matnx, @Viewport, wx, wy, wz) ;
ShowMessage ('Мировые координаты для z=' + FloatToStr(Zval)
+ ' : ' + chr (13) + '(' + FloatToStr(wx)
+ '; ' + FloatToStr(wy) + '; '
+ FloatToStr(wz) + ')');
end;

Команда giuUnProject требует задания трех оконных координат - X, Y, Z Третья координата здесь - значение буфера глубины соответствующего пиксела, для получения которого пользуемся командой glReadPixels, указав в качестве аргументов координаты пиксела и задав размер области 1x1.
Обратите внимание, что при нажатии кнопки на пустом месте значение буфера глубины максимально и равно единице, для точек, наименее удаленных от точки зрения, значение буфера глубины минимальное
Команда giuUnProject имеет парную команду - giuProject, переводящую координаты объекта в оконные координаты Для ознакомления с этой командой предназначен проект из подкаталога Ех21 Действие приложения заключается в том, что в пространстве по кругу перемещается точка, а ее оконные координаты непрерывно выводятся в компоненте класса тмето в правой части экрана (рис 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;
mvMatnx, ProjMatnx : Array [0..15] of GLdouble;
wx, wy, wz : GLdouble; // оконные координаты
begin
// заполнение массивов матриц
glGetlntegerv (GL_VIEWPORT, @Viewport);
glGetDoublev (GL_MODELVIEW_MATRIX, SmvMatrix),
glGetDoublev (GL_PROJECTION_MATRIX, SProjMatnx);
// перевод координат объекта в оконные координаты
gluProject (0, 0, -0.5, SmvMatrix, gProjMatrix, SViewport, 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;

Первые три аргумента команды giuProject - мировые координаты точки, следующие три аргумента - массивы с характеристиками области вывода и матриц, последние три аргумента - возвращаемые оконные координаты
Учтите, что по оси Y возвращаемую координату требуется преобразовать к обычной оконной системе координат, здесь я вывожу значение выражения (ClientHeight - wy). Следя указателем курсора за точкой, легко убедиться в полном соответствии результатов

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

Рис. 6.12. В режиме обратной связи библиотека OpenGL уведомляет о всех своих действиях

В пространстве крутятся площадка и точка над ней, в компоненте класса тмето выводится информация о каждой воспроизводимой вершине. Сразу же обращаем внимание, что говорится о воспроизведении двух многоугольников по трем вершинам и одной отдельной вершины - точки, или примитиву типа 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. Если аргумент равен второму возможному значению, то перед воспроизведением каждого примитива в буфер обратной связи вызовом команды giPassThrough помещается маркер:

procedure Render (mode: GLenum);
begin
If mode = GL_FEEDBACK then giPassThrough(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 giPassThrough(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 ] := 0 to 6 do begin
vert := vert + Format('%4.2f ', [b[n-i]]); DEC(i);
end;
Memol.Lines.Add(vert);
end;
end;
end;

Из комментариев, надеюсь, становится ясно, как анализировать содержимое буфера обратной связи.
Для сокращения кода я реализовал анализ только для двух типов - точек и полигонов. В документации по команде giFeedbackBuffer вы найдете описание всех остальных типов, используемых в этом буфере.
Чтобы легче было разбираться, я предусмотрел остановку движения объектов по нажатию клавиши пробела и вывод координат курсора в заголовке окна. Обратите внимание на две вещи - на то, что оконная координата вершин по оси 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(8ob]ects[i].vl) ;
glVertex2fv(@ob]ects[i].v2);
glVertex2fv(@ob;jects[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, ob]x, 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, позволяющие устанавливать точку зрения в любое место в пространстве, а также компонент того же класса, позволяющий менять длину осевых линий.
Кроме того, на панели располагаются кнопки, отвечающие за чтение/запись проектируемой модели, а также кнопка "Освежить" - перерисовка экрана. Внизу панели помещены несколько компонентов класса тспескВох, задающих режимы рисования: наличие осей, сетки, тумана и площадки.
Так, по нажатию на кнопки элемента с надписью "Расстояние" можно приближать или удалять точку зрения к пространству модели, элементы "Сдвиг" позволяют передвигать точку зрения по соответствующей оси, а элементы "Угол" - поворачивать модель в пространстве относительно указанной оси Пара замечаний по поводу этих действий. Перспектива задается с помощью Команды 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;

Перевод здесь условный, в результате него получаются только две пространственные координаты из трех. Первые два аргумента процедуры - экранные координаты. Для перевода в пространственные координаты используется то,
что перспектива задается с помощью команды giFrustum. Координаты крайних точек окна выражаются через координаты плоскостей отсечения, передаваемые экранные координаты приводятся к этому интервалу. При изменении размеров площадки с помощью этой процедуры получаются условные пространственные координаты для предыдущего и текущего положений курсора. Приращение этих координат дает величину, на которую пользователь сместился в пространстве. Но это условная величина, и при переводе в реальное пространство требуется ее корректировка (в рассматриваемой программе при переводе она умножается на 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, это собственный формат модельера. Файлы второй группы имеют расширение Лпс, это текстовые файлы, представляющие собой готовые куски программы, пригодные для включения в шаблоны программ 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;

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

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

к оглавлению