ГЛАВА 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;

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