Библиотека трехмерной графики OpenGL Библиотека трехмерной графики Open GL Южный Федеральный Университет Фомин Г.В. Дается последовательное описание функций и процедур базовой библиотеки компьютерной 3-мерной графики OpenGL. Описание сопровождается интерактивными иллюстрациями, демонстрирующими эффект применения этих функций. Приводятся исходные коды иллюстраций на языке C# v. 3.0 в среде MS Visual Studio 2008. 2 Оглавление Введение ............................................................................................................................................................ 3 Подготовка файловой системы проекта...................................................................................................... 4 Подготовка формы проекта .......................................................................................................................... 5 Форма сообщений об ошибках .................................................................................................................... 7 Первые команды ............................................................................................................................................... 8 Класс контекста воспроизведения ............................................................................................................... 8 Класс команд OpenGL. Очистка буфера цвета ..........................................................................................16 Структура буферов. Методы Get ................................................................................................................18 Метод Get. Получение информации о состоянии OpenGL. .................................................................19 Метод GetString. ......................................................................................................................................20 Методы GetError и gluErrorString ...........................................................................................................21 Примитивы .......................................................................................................................................................24 Модуль 0. Точки...........................................................................................................................................24 Методы Begin, End. Вершины. Примитив POINTS .................................................................................24 1 Атрибуты точки .....................................................................................................................................30 Метод PointSize ....................................................................................................................................30 Сглаживание. Методы Enable, Disable и IsEnable .............................................................................31 Методы PushAttrib, PopAttrib .............................................................................................................33 Цвет вершины. Метод Color ...............................................................................................................34 Фильтр цвета. Метод ColorMask ........................................................................................................35 2 Альфа-тест. Метод AlphaFunc ..............................................................................................................36 3 Объектные координаты вершин .........................................................................................................38 4 Оконные координаты. Порт наблюдения. Метод Viewport .............................................................39 Метод Project .......................................................................................................................................40 5 Тест глубины .........................................................................................................................................42 Методы ClearDepth и DepthFunc ........................................................................................................44 6 Преобразование к координатам отсечения.......................................................................................45 Методы MatrixMode............................................................................................................................46 Методы LoadIdentity и Ortho ..............................................................................................................47 7 Методы преобразования от объектных координат к координатам наблюдения .........................49 Методы Translate, Rotate ....................................................................................................................50 Метод Scale ..........................................................................................................................................51 Метод LookAt .......................................................................................................................................52 8 Дисплейные списки. Методы IsList, GenLists, DeleteLists, NewList, EndList, CallList ........................57 9 Вывод символов. GDI-функции ...........................................................................................................63 10 BMP-символы ......................................................................................................................................64 Методы ListBase и CallLists ..................................................................................................................64 Методы RasterPos и UseFontBitmap ...................................................................................................65 11 Контурный шрифт ...............................................................................................................................68 Метод UseFontOutline .........................................................................................................................68 Методы PushMatrix, PopMatrix...........................................................................................................71 Модуль 1. Прямые линии ...........................................................................................................................72 1.0 Примитив LINES ..................................................................................................................................72 Метод LineWidth ..................................................................................................................................73 3 Сглаживаемость линии .......................................................................................................................73 Метод LineStipple .................................................................................................................................74 1.1 Примитивы LINE STRIP и LINE LOOP ..................................................................................................75 Модуль 2. Треугольники .............................................................................................................................76 2.0 Примитив "TRIANGLES"......................................................................................................................76 Метод FrontFace..................................................................................................................................77 Метод CullFace .....................................................................................................................................77 Метод PolygonMode ............................................................................................................................78 Метод PolygonStipple ..........................................................................................................................79 2.1 Примитивы TRIANGLE STRIP и TRIANGLE FAN ..................................................................................80 2.2 Дисплейные списки сферы и тора ...................................................................................................81 Сфера ....................................................................................................................................................82 Тор.........................................................................................................................................................84 Модуль 3. 4-угольники ................................................................................................................................86 3.0 Примитив QUADS ...............................................................................................................................86 3.1 Примитив QUAD STRIP .......................................................................................................................87 3.2 Дисплейный список усеченного конуса ..........................................................................................88 Модуль 4. Многоугольники .......................................................................................................................91 4.0 Примитив POLYGON...........................................................................................................................91 4.1 Смешивание цветов. Прозрачность .................................................................................................93 4.2 Отсечение. Метод ClipPlane ..............................................................................................................95 4.3 Трафарет. Методы StencilOp и StencilFunc ......................................................................................96 4.4 Эффекты буфера-аккумулятора. Метод Accum.............................................................................102 4.5 Перспективная проекция ................................................................................................................104 Методы Frustum и Perspective..........................................................................................................104 Туман. Метод Fog. .............................................................................................................................106 4.6 Scissor Test ........................................................................................................................................108 Remaining........................................................................................................................................................110 Модуль 5. Освещение (lighting) ................................................................................................................110 5.0 Среда и вещество ............................................................................................................................110 Метод LightModel ..............................................................................................................................110 Метод Material ...................................................................................................................................111 Метод ColorMaterial ..........................................................................................................................113 Метод ShadeModel ............................................................................................................................114 5.1 Источники света ...............................................................................................................................115 Введение OpenGL (Open Graphics Library – открытая графическая библиотека) содержит несколько сотен процедур и функций, которые обеспечивают формирование любого 3-мерного образа на экране компьютера. Библиотека OpenGL не требует специальной установки. Все ее процедуры реализованы в файлах opengl32.dll и glu32.dll, поставляемых вместе с OS Windows в каталоге System32. Полную спецификацию команд, реализованных в модуле opengl32.dll, можно найти на сайте. Так же на сайте можно найти презентацию этого курса. В настоящем пособии изложение процедур и функций библиотеки OpenGL сопровождается интерактивными иллюстрациями и примерами конкретного кода на языке C# с применением этих 4 функций. Программный код в целом образует решение (solution), состоящее из отдельных проектов, участвующих в иллюстрирующем приложении. Читатель может либо ограничиться общим знакомством с командами библиотеки OpenGL, либо пойти дальше и поддерживать свое приложение, тестирующее работу отдельных команд OpenGL, используя для этого примеры, описанные в учебнике, либо, наконец, следуя рекомендациям автора, использовать приведенный им код и участвовать в составлении авторского приложения, иллюстрирующего работу команд OpenGL. Типичная программа, которая использует команды OpenGL, начинается с создания окна, получающего информацию из области памяти, которая именуется видеобуфером, или буфером кадров (frame buffer). Этому окну сопоставляется специальный контекст воспроизведения (rendering context), необходимый для визуализации работы команд библиотеки OpenGL. Контекст воспроизведения превращает окно в порт вывода команд OpenGL. После создания порта окно готово для визуализации команд OpenGL. Одни команды OpenGL строят простые геометрические объекты (так называемые примитивы) точки, сегменты линий, многоугольники. Другие команды определяют внешний вид этих объектов, их цвета, как они освещаются и как проецируются с 3-мерного пространства на 2-мерный экран. В следующих трех разделах введения описаны технические действия, необходимые для создания приложения, иллюстрирующего работу команд OpenGL. Подготовка файловой системы проекта Откройте MS Visual Studio 2008 и создайте пока пустой контейнер (solution), в который будут помещаться приложения, иллюстрирующие работу команд графической библиотеки OpenGL. Для этого 1. В меню File командой New->Project… откройте окно New Project 2. На левой панели Project types: найдите и откройте узел Other Project Types 3. Выберите Visual Studio Solutions. 4. На панели Templates: выберите Blank Solution. 5. В окошке Name имя Solution1, задаваемое средой по умолчанию, замените slOpenGL. 6. В строке Location указана маршрутная строка, где будет располагаться папка с именем slOpenGL, содержащая все файлы, входящие в состав slOpenGL; обычно строка Location соответствует маршруту, выводящему на папку Visual Studio 2008\Projects, но удобнее, чтобы не мешать другим пользователям, работающим в этой же среде, организовать внутри каталога Projects новый каталог со своим именем, сохраняя свои проекты в этом подкаталоге. 7. Нажмите OK. Добавьте в контейнер slOpenGL два проекта. Для этого 1. Откройте окно Solution Explorer. 2. Вызовите контекстное меню (правая кнопка) над строкой Solution ' slOpenGL' (0 projects). 3. Командой Add->New Project… откройте окно Add New Project. 4. На панели Templates: выберите шаблон Class Library. 5. В окошке Name наберите GL. Это будет имя библиотеки, в которую будут собираться классы графической библиотеки в процессе работы над проектом. 6. Нажмите OK. 7. Чтобы добавить еще один проект, повторите пункты 1-3. 8. На панели Templates: выберите шаблон Windows Forms Application. 9. Дайте имя gl3D. Эти файлы будут главной формой иллюстрирующего приложения. 5 10. Нажмите OK. Откройте вновь окно Solution Explorer и в нем сделайте проект gl3D стартовым. Для этого откройте контекстное меню над именем этого проекта и выберите в нем команду Set As StartUp Project. Шрифт имени проекта gl3D должен стать полужирным. Теперь по команде Start среда будет активировать именно проект gl3D. В том же окне Solution Explorer командой Rename из контекстного меню измените имена файлов следующим образом Class1.cs -> GL.cs, Form1-> f3D, Program.cs -> pr3D.cs, соглашаясь с предложениями среды. Рекомендация Для сохранения проекта на внешнем носителе (флэш-карте и т.п.) необходимо, вообще говоря, сохранять весь каталог решения slOpenGL. Для этого рекомендуется в начале архивировать всю папку slOpenGL в один файл (.rar, .zip и т.п.), который затем переписать на внешний носитель, либо отослать по почте. При работе в общей аудитории это необходимо делать уже потому, что никто не гарантирует сохранность информации в Ваше отсутствие. Подготовка формы проекта Замените значение Form1 в заголовке формы на 3D. Для этого 1. Войдите в окно визуального отображения формы f3D.cs [Design]. Там расположено изображение окна проекта с заголовком Form1 по умолчанию. 2. Щелкните по окну формы мышкой. 3. Откройте окно Properties. 4. Найдите в левой колонке свойство Text. 5. В правой колонке наберите 3D. Поместите на форму компоненту-контейнер ToolStripContainer. Для этого 1. Откройте окно ToolBox, где находятся все компоненты, доступные для визуального проектирования. 2. Найдите и откройте раздел Menus&Toolbars. 3. Выберите компоненту ToolStripContainer и перетащите ее на форму. 4. Нажмите команду Dock Fill in Form. По этой команде контейнер займет всю клиентскую область окна формы и будет постоянно так расположен вне зависимости от размеров формы. Компонента ToolStripContainer объединяет 5 панелей - одну в центре и 4 по краям. Все панели по умолчанию присутствуют и видны. Центральная панель этой компоненты называется Contentпанелью. На нее поместите новую компоненту Panel. Для этого 1. Войдите вновь в окно ToolBox 2. Откройте раздел Containers. 3. Найдите компоненту Panel и перетащите ее на центральную панель формы. 4. Щелкните по кнопке, имеющей форму стрелки, в правом верхнем углу контура панели. 5. Дайте команду Dock in Parent Container. Новая панель полностью заполнит Content-панель контейнера ToolStripContainer1. 6. Откройте окно Properties, найдите свойство Name новой панели и замените значение panel1, стоящее там по умолчанию, значением panelGL. На панель panelGL будет выводиться изображение, формируемое командами OpenGL. Для этого ей будет сопоставлен "порт вывода изображения", или "контекст визуализации" (rendering context), необходимый для работы команд OpenGL. Панели, расположенные по краям компоненты ToolStripContainer, будут использоваться в дальнейшем для размещения элементов управления (меню, строки статуса, кнопок, редакционных окошек и т.д.). В частности, добавьте на нижнюю панель строку статуса, для чего 6 1. Откройте окно ToolBox, где находятся все компоненты, доступные для визуального проектирования. 2. Найдите и откройте раздел Menus&Toolbars. 3. Выберите компоненту StatusStrip и перетащите ее на нижнюю панель. 4. В свойство Items образовавшегося объекта StatusStrip1 добавьте объект типа StatusLabel, который назовите stLabel. В него будет помещаться текстовая информация. 5. В окне Properties у вновь образованной компоненты stLabel (для выделения компоненты в верхней части окна Properties есть выпадающий список) сотрите длинное значение свойства Text, установленное средой по умолчанию. Измените стартовое положение окна на экране дисплея. Для этого 1. Перейдите в окно дизайнера f3D.cs [Design]. 2. Откройте окно Properties . 3. Из выпадающего списка в верхней части окна Properties установите объект формы f3D, свойства которого намерены изменить. 4. В списке свойств объекта f3D найдите StartPosition. 5. В правой колонке установите CenterScreen. Теперь окно будет всегда открываться в центре экрана. Описание класса формы f3D среда размещает в двух файлах f3D.cs и f3D.Designer.cs. То, что описание класса разнесено на несколько файлов, указывается модификатором partial, стоящим в заголовке описания. Файл f3D.cs содержит ту часть описания, которая управляется непосредственно пользователем. Файл f3D.Designer.cs содержит описание тех членов класса, которыми управляет дизайнер среды в процессе визуального конструирования. Этот последний файл не рекомендуется редактировать. Конструктор класса f3D находится в пользовательском файле f3D.cs. Его тело состоит по умолчанию только из вызова метода InitializeComponent(), описанного в файле дизайнера f3D.Designer.cs. После вызова метода InitializeComponent() пользователь может помещать в тело конструктора формы любые операторы, которые необходимы на стадии конструирования объекта формы. В данном случае поместите в тело конструктора после строки InitializeComponent(); операторы, которые делают форму квадратной и максимально заполняющей экран вне зависимости от его размеров и формы: // Операторы, делающие форму квадратной со стороной side, // равной минимальной стороне дисплея. // Класс Screen представляет дисплей или несколько дисплеев. // Статическое свойство PrimaryScreen класса Screen возвращает главный дисплей // Свойство Bounds возвращает прямоугольник границ дисплея int side = Math.Min(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); // Свойство Size определяет размеры окна Size = new Size(side, side); Само тело конструктора f3D() поместите в раздел (region) с именем Ctr (принятое сокращение от слова Constructor) так, чтобы весь код принял следующий вид #region Ctr public f3D() { InitializeComponent(); // Операторы, делающие форму квадратной со стороной side, // равной минимальной стороне дисплея. // Класс Screen представляет дисплей или несколько дисплеев. // Статическое свойство PrimaryScreen класса Screen возвращает главный дисплей // Свойство Bounds возвращает прямоугольник границ дисплея 7 int side = Math.Min(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); // Свойство Size определяет размеры окна Size = new Size(side, side); } #endregion Проверьте результат проектирования, активировав приложение командой Start Debugging из меню Debug (кнопка с зеленой стрелочкой на панели быстрого доступа). Форма сообщений об ошибках При отладке нового приложения практически неизбежны ошибки времени выполнения. Для вывода на экран сообщений о возможных ошибках, можно добавить к проекту форму, которая будет отображаться каждый раз, когда в цикле главной формы возникают ошибки. Добавьте к проекту gl3D новую форму. Для этого 1. В окне Solution Explorer получите контекстное меню на строке gl3D. 2. Командой Add->Windows Form откройте окно Add New Item. 3. В строке Name поставьте имя файла новой формы ErrorMsgForm.cs и нажмите OK 4. Измените имя вновь созданной формы на ErrorMsgForm в окне Properties новой формы 5. В том же окне установите свойство формы StartPosition в CenterScreen 6. Из окна Tools перенесите на форму объект класса RichBoxText 7. Назовите этот объект errMsgTextBox 8. В окне Properties установите его свойство Modifiers в public 9. Там же установите его свойство Dock в Fill Теперь проведите редакцию файла проекта pr3D.cs: 1. Добавьте строку using System.IO в самое начало файла 2. Вместо строки Application.Run поместите следующий код try { Application.Run(new f3D()); } catch (Exception e) { if ( MessageBox.Show ( e.Message + "\nДетали?", Path.GetFileName(Application.ExecutablePath) + " Ошибка!!", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1 ) == DialogResult.Yes ) { // Если запрошены детали, // создается окно с редактором errMsgTextBox, в который выводится // описание ошибки и детальный маршрут к функции, внутри которой ошибка возникла gl3D.ErrorMsgForm emf = new gl3D.ErrorMsgForm(); 8 // В errMsgTextBox помещается строка с подробным сообщением об ошибке emf.errMsgTextBox.Text = e.ToString(); // Окно изображается в модальном режиме // (пока окно не закроется, фокус не переводится на другие окна) emf.ShowDialog(); // Окно уничтожается в памяти emf.Dispose(); } Application.Exit(); } Для описания ошибок времени выполнения будет использоваться механизм объектов исключительной ситуации. Этот механизм предполагает, что при возникновении ошибки в операторах, находящихся в блоке try (в данном случае практически в любом месте выполняемой программы, т.к. в блоке try находится основной метод Application.Run с формой f3D), управление передается в блок catch, описанный здесь. Параметром блока catch служит объект "e" класса исключительной ситуации Exception, порожденный ошибочным кодом, или намеренно созданный программистом. У объектов класса Exception есть свойство Message, содержащее краткую информацию об ошибке. Метод ToString() объектов класса Exception возвращает полное описание маршрута к источнику ошибки. Проверка работы этого механизма будет проведена ниже на конкретном примере. Скомпилируйте новую версию проекта. Первые команды Комплексная цель модуля Познакомиться с тем, как создается контекст воспроизведения, необходимый для вывода результатов работы команд OpenGL в окно (на панель). Познакомиться со структурой буферов – разделов памяти, хранящих информацию об отдельных элементах изображения – пикселях. Класс контекста воспроизведения В этом разделе Описан класс, позволяющий сопоставить окну (в данном случае панели формы f3D) контекст воспроизведения команд OpenGL. Выполнение методов этого класса является необходимым условием получения изображения. В конструкторе формы f3D создается экземпляр класса контекста воспроизведения, сопоставляемый панели. Этим завершается подготовка окна (панели) к воспроизведению команд OpenGL. Для использования команд библиотеки OpenGL следует сопоставить панели panelGL формы f3D так называемый контекст воспроизведения. Контекст воспроизведения является портом, через который команды OpenGL будут выводить результат своей работы в окно (в данном случае на панель panelGL). С этой целью 1. В окне Solution Explorer в раздел References библиотеки GL командой Add References… через контекстное меню добавьте ссылку на библиотеку System.Windows.Forms из вкладки .NET 2. В начале созданного Вами библиотечного файла GL.cs добавьте операторы ссылок на две библиотеки, т.е. две строки 9 using System.Runtime.InteropServices; using System.Windows.Forms; 3. Уберите скелет класса GL (имя класса с фигурными скобками), предложенный средой по умолчанию 4. Добавьте код нового класса glPort следующего содержания /// <summary> /// Управляет созданием и освобождением контекста воспроизведения OpenGL /// </summary> public class glPort : IDisposable { /// <summary> /// Буфер дублирован. /// </summary> const uint PFD_DOUBLEBUFFER = 0x00000001; /// <summary> /// Буфер может передавать содержание в окно или на поверхность устройства /// </summary> const uint PFD_DRAW_TO_WINDOW = 0x00000004; /// <summary> /// Буфер поддерживает команды OpenGL. /// </summary> const uint PFD_SUPPORT_OPENGL = 0x00000020; /// <summary> /// Описывает формат пикселей изображающей поверхности. /// </summary> [StructLayout(LayoutKind.Sequential)] class PIXELFORMATDESCRIPTOR { internal short nSize = 40; internal short nVersion = 1; internal uint dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL |PFD_DOUBLEBUFFER; internal byte iPixelType; internal byte cColorBits = 32; internal byte cRedBits; internal byte cRedShift; internal byte cGreenBits; internal byte cGreenShift; internal byte cBlueBits; internal byte cBlueShift; internal byte cAlphaBits = 8; internal byte cAlphaShift; internal byte cAccumBits = 8; internal byte cAccumRedBits; internal byte cAccumGreenBits; internal byte cAccumBlueBits; internal byte cAccumAlphaBits; internal byte cDepthBits = 32; internal byte cStencilBits = 8; internal byte cAuxBuffers; 10 internal byte iLayerType; internal byte bReserved; internal int dwLayerMask; internal uint dwVisibleMask; internal uint dwDamageMask; } #region Поля и свойства /// <summary> /// Хранит идентификатор библиотечного модуля OpenGL /// </summary> static int openGLdllHandle; /// <summary> /// Хранит дескриптор формата пикселей /// </summary> PIXELFORMATDESCRIPTOR pixelFD = new PIXELFORMATDESCRIPTOR(); /// <summary> /// Устанавливает и возвращает текущий контекст устройства /// </summary> public int deviceContext { private set; get; } /// <summary> /// Хранит текущий контекст воспроизведения /// </summary> int renderContext; /// <summary> /// Хранит идентификатор (хэндл) окна /// </summary> int controlHandle; #endregion #region Статические методы #region Методы установки формата пикселей /// <summary> /// Пытается найти формат пикселей, поддерживаемый данным контекстом устройства и /// наиболее близким к данному формату. /// </summary> /// <param name="dvcContext"> /// Определяет контекст устройства, для которого должен быть выбран формат пикселей. /// </param> /// <param name="pfd"> /// Ссылка на экземпляр структуры типа PIXELFORMATDESCRIPTOR, /// который определяет требуемый формат пикселя /// </param> /// <returns> /// Индекс искомого формата пикселей. Если функция выполнена успешно, /// то это значение больше нуля. /// Если нет, то значение равно нулю. 11 /// </returns> [DllImport("GDI32")] static extern int ChoosePixelFormat(int dvcContext, [MarshalAs(UnmanagedType.LPStruct)]PIXELFORMATDESCRIPTOR pfd); /// <summary> /// устанавливает формат пикселя указанного контекста устройства в формат, /// определенный индексом pixelFormatIndex /// </summary> /// <param name="dvcContext"> /// Контекст устройства, для которого устанавливается данный формат пикселя /// </param> /// <param name="pixelFormatIndex"> /// Индекс формата пикселя /// </param> /// <param name="pfd"> /// Ссылка на структуру /// (в данном случае класс, который должен интерпретироваться как структура) типа /// PIXELFORMATDESCRIPTOR, которая содержит спецификацию формата пикселя. /// </param> /// <returns> /// При успешном завершении возвращает TRUE, в противном случае FALSE. /// </returns> [DllImport("GDI32")] static extern bool SetPixelFormat(int dvcContext, int pixelFormatIndex, [MarshalAs(UnmanagedType.LPStruct)]PIXELFORMATDESCRIPTOR pfd); #endregion #region Методы манипуляции с контекстами /// <summary> /// Возвращает идентификатор контекста устройства дисплея для клиентской области окна. /// Контекст может быть использован при вызове функций, /// создающих изображение на клиентской области окна. /// </summary> /// <param name="handle"> /// идентификатор (хэндл) окна, чей контекст должен быть возвращен /// </param> /// <returns> /// При успешном завершении возвращает идентификатор контекста устройства, /// в противном случае 0 /// </returns> [DllImport("User32")] static extern int GetDC(int handle); /// <summary> /// Освобождает контекст устройства для использования другими приложениями. /// </summary> /// <param name="handle"> /// идентификатор (хэндл) окна, чей контекст должен быть освобожден /// </param> /// <param name="dvcContext"> /// идентификатор освобождаемого контекста устройства 12 /// </param> /// <returns> /// При успешном завершении возвращает 1, в противном случае 0 /// </returns> [DllImport("User32")] static extern int ReleaseDC(int handle, int dvcContext); /// <summary> /// Создает новый контекст воспроизведения OpenGL. /// Контекст воспроизведения соответствует устройству с заданным контекстом. /// Контекст воспроизведения имеет тот же формат пикселя, что и заданный контекст устройства. /// </summary> /// <param name="dvcContext"> /// Идентификатор контекста устройства /// </param> /// <returns> /// При успешном завершении возвращает идентификатор контекста воспроизведения, /// в противном случае 0 /// </returns> [DllImport("OPENGL32.DLL")] static extern int wglCreateContext(int dvcContext); /// <summary> /// Уничтожает контекст воспроизведения /// </summary> /// <param name="rndContext"> /// Идентификатор контекста воспроизведения /// </param> /// <returns> /// При успешном завершении возвращает TRUE, в противном случае FALSE. /// </returns> [DllImport("OPENGL32.DLL")] static extern bool wglDeleteContext(int rndContext); /// <summary> /// Производит обмен содержимого переднего и заднего буферов, /// если формат пикселя указанного /// контекста устройства включает задний буфер (двойная буферизация) /// </summary> /// <param name="dvcContext"> /// Идентификатор контекста устройства /// </param> /// <returns> /// При успешном завершении возвращает TRUE, в противном случае FALSE. /// </returns> [DllImport("GDI32")] static extern bool SwapBuffers(int dvcContext); /// <summary> /// Делает указанный контекст воспроизведения OpenGL текущим контекстом /// для данного подпроцесса (нити). /// Все последующие вызовы команд OpenGL, сделанные в этом подпроцессе, /// будут выполняться на устройстве 13 /// с указанным контекстом. Функция может использоваться так же для того, чтобы текущий /// контекст воспроизведения перестал быть текущим /// </summary> /// <param name="dvcContext"> /// Идентификатор контекста устройства /// </param> /// <param name="rndContext"> /// Идентификатор контекста воспроизведения. Если это значение 0, /// то текущий контекст воспроизведения перестает быть текущим. /// В этом случае второй параметр игнорируется. /// </param> /// <returns> /// При успешном завершении возвращает TRUE, в противном случае FALSE. /// </returns> [DllImport("OPENGL32.DLL")] static extern bool wglMakeCurrent(int dvcContext, int rndContext); #endregion /// <summary> /// Вводит исполняемый модуль в адресное пространство вызываемого процесса /// </summary> /// <param name="funcname"> /// Имя исполняемого модуля /// </param> /// <returns> /// При успешном завершении возвращает идентификатор (хэндл) загруженного модуля, /// в противном случае 0 /// </returns> [DllImport("Kernel32")] public static extern int LoadLibrary(String funcname); #endregion /// <summary> /// Вызывает метод wglMakeCurrent для данных контекстов устройства и воспроизведения /// </summary> /// <returns> /// Результат, возвращаемый методом wglMakeCurrent /// </returns> public bool MakeCurrent() { return wglMakeCurrent(deviceContext, renderContext); } /// <summary> /// Вызывает метод SwapBuffers для данного контекста устройства /// </summary> /// <returns> /// Возвращает результат метода SwapBuffers /// </returns> public bool SwapBuffers() { return SwapBuffers(deviceContext); 14 } /// <summary> /// Определяет идентификатор данного окна, создает контекст устройства, /// контекст воспроизведения и делает контекст воспроизведения текущим. /// </summary> /// <param name="control"> /// данное окно /// </param> public glPort(Control control) { // Получение контекста устройства if (0 == (deviceContext = GetDC(controlHandle = (int)control.Handle))) throw new Exception("Контекст устройства для данного окна не определен."); // загрузка библиотеки OpenGL if (openGLdllHandle == 0) if (0 == (openGLdllHandle = LoadLibrary("OpenGL32.DLL"))) throw new Exception("Библиотека OPENGL32 не включена в процесс."); // получение индекса требуемого формата пикселя int pixelFormatIndex = ChoosePixelFormat(deviceContext, pixelFD); if (0 == pixelFormatIndex) throw new Exception("Формат пикселей не определен."); // установка формата пикселя для данного контекста устройства if (!SetPixelFormat(deviceContext, pixelFormatIndex, pixelFD)) throw new Exception("Формат пикселей не установлен."); // создание контекста воспроизведения OpenGL для данного контекста устройства if (0 == (renderContext = wglCreateContext(deviceContext))) throw new Exception("Контекст воспроизведения не установлен."); // контекст воспроизведения делается текущим if (!wglMakeCurrent(deviceContext, renderContext)) throw new Exception("Контекст не установлен текущим."); } /// <summary> /// Уничтожает контекст воспроизведения и освобождает контекст устройства текущего окна /// </summary> public void Dispose() { // контекст воспроизведения перестает быть текущим if (!wglMakeCurrent(0, 0)) throw new Exception("Контекст не установлен нулем."); if (renderContext > 0) // уничтожается контекст воспроизведения if (!wglDeleteContext(renderContext)) throw new Exception("Контекст воспроизведения не освобожден."); if (deviceContext > 0) // освобождается контекст устройства if (0 == ReleaseDC(controlHandle, deviceContext)) throw new Exception("Контекст устройства не освобожден."); renderContext = deviceContext = 0; } 15 } Перенесите этот код и ознакомьтесь с комментариями. Скомпилируйте решение. К ссылкам проекта gl3D добавьте ссылку на библиотеку GL (контекстное меню над пунктом References проекта gl3D, команда Add references…, вкладка Projects). Для использования класса glPort с панелью panelGL перейдите к редактированию файла главной формы проекта f3D.cs. 1. В заголовке файла добавьте ссылку на библиотеку GL командой using GL; 2. В тело класса f3D добавьте раздел "Рабочие поля и свойства" с описанием поля port #region Рабочие поля и свойства /// <summary> /// Хранит текущий порт воспроизведения команд OpenGL /// </summary> glPort port; #endregion 3. В тело конструктора формы (метод f3D) после операторов установки размеров формы добавьте оператор создания объекта port = new glPort(panelGL); Этот оператор должен создать контекст воспроизведения команд OpenGL на панели panelGL сразу же после создания формы и основных инициализирующих действий. 4. Выберите окно дизайнера и в окне Properties для формы f3D назначьте обработчик событию FormClosed (вкладка с изображением молнии содержит обработчики событий), дважды щелкнув по полю справа. 5. Среда откроет тело обработчика этого события с именем f3D_FormClosed. Добавьте в него код освобождения контекстов port.Dispose(); Этот код освободит контексты после закрытия формы. Обработчик поместите в отдельный раздел (region), назвав этот раздел «Обработчики формы» 6. В тело класса формы f3D добавьте раздел "Методы воспроизведения" с описанием метода RenderFrame #region Методы воспроизведения /// <summary> /// Устанавливает воспроизводимую сцену и копирует буферы /// </summary> void RenderFrame() { if (!port.SwapBuffers()) throw new Exception("Не выполнен обмен буферов."); } #endregion 7. В окне Properties найдите объект panelGL и добавьте обработчик события Paint. 8. В тело обработчика добавьте оператор вызова созданного метода RenderFrame(); Этот вызов перенесет содержимое заднего буфера панели PanelGL в передний буфер, передав тем самым изображение на экран. Обработчик panelGL_Paint поместите в раздел "Обработчики panelGL" так, что результат редакции будет выглядеть следующим образом #region Обработчики panelGL private void panelGL_Paint(object sender, PaintEventArgs e) { RenderFrame(); } #endregion 16 Активируйте проект. Если все сделано верно, должны увидеть черную панель. Пройдите по коду проекта шагами с помощью команды отладчика Step Into из меню Debug. Эта процедура позволит увидеть последовательность выполнения операторов проекта. Чтобы понять, как работает механизм реакции на ошибки в созданном коде, в библиотеке GL закройте комментаторскими скобками /* … */ строки загрузки библиотеки OpenGL в теле конструктора glPort. Это строки if (openGLdllHandle == 0) if (0 == (openGLdllHandle = LoadLibrary("OpenGL32.DLL"))) throw new Exception("Библиотека OPENGL32 не включена в процесс."); Активируйте проект. Результатом должно быть появление окна сообщения об ошибке типа "Контекст воспроизведения не установлен". Вновь пройдите по шагам код проекта командой Step Into. Уберите комментаторские скобки и вернитесь к работающей версии проекта. Тест рубежного контроля 1. Что описывает класс PIXELFORMATDESCRIPTOR? 2. Что делает метод SetPixelFormat? 3. Каковы параметры метода MakeCurrent? 4. Что делает метод CreateContext? 5. Что возвращает метод ChoosePixelFormat? 6. Как используется метод SwapBuffers? 7. Как сопоставить контексты воспроизведения двум различным панелям? 8. Укажите верную последовательность использования методов, перечисленных в вопросах 2-5. 9. Почему необходимо вызывать метод RenderFrame в обработчике перерисовки панели? 10. Опишите последовательность выполнения методов в форме f3D на протяжении всего жизненного цикла формы. Класс команд OpenGL. Очистка буфера цвета В этом разделе Создается класс команд OpenGL, методы которого будут служить оболочками процедур и функций библиотеки OpenGL. Применяются команды очистки буфера цвета ClearColor и Clear. Панель имеет черный цвет, так как по умолчанию цвет всех пикселей в буфере цвета черный – все биты буфера равны нулю. Команды OpenGL позволяют устанавливать произвольный цвет заполнения буфера цвета. Это цвет очистки сцены, или фон изображения. Откройте библиотеку классов GL и опишите новый статический класс с именем gl, в который будут помещаться методы, отвечающие командам OpenGL. Поместите в класс gl члены, регулирующие цвет заполнения буфера цвета. Эти члены будут объединены областью, названной "Очистка буферов". Описание класса gl примет вид public static class gl { #region Очистка буферов /// <summary> /// Определяет значения цветов в буферах цвета. /// По умолчанию все 4 составляющих цвета равны нулю /// </summary> /// <param name="red"> /// количество красного от 0 до 1 17 /// </param> /// <param name="green"> /// количество зеленого от 0 до 1 /// </param> /// <param name="blue"> /// количество синего от 0 до 1 /// </param> /// <param name="alpha"> /// количество альфа-компоненты от 0 до 1, участвующей в определении эффекта прозрачности /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glClearColor")] public static extern void ClearColor(float red, float green, float blue, float alpha); /// <summary> /// Идентифицирует буфер цвета в методах Clear и PushAttrib. /// </summary> public const int COLOR_BUFFER_BIT = 0x00004000; /// <summary> /// Очищает буферы внутри порта вывода изображения /// </summary> /// <param name="mask"> /// Выражение, идентифицирующее буферы, которые должны быть очищены. /// Четыре маски COLOR_BUFFER_BIT, DEPTH_BUFFER_BIT, /// ACCUM_BUFFER_BIT и STENCIL_BUFFER_BIT могут быть связаны оператором | (или) /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glClear")] public static extern void Clear(int mask); #endregion } Везде в дальнейшем методы и постоянные, относящиеся к командам OpenGL, будут размещаться в этом классе В классе формы f3D опишите постоянную цвета, которым будет очищаться буфер цвета #region Constants /// <summary> /// Хранит цвет фона - цвет очистки буфера цвета (темно-синий) /// </summary> static Color clearColor = Color.FromArgb(0, 0, 51); #endregion Опишите так же два новых метода InitGL и BuildFrame. Метод InitGL поместите в раздел "Ctr" следом за описанием конструктора и в него поместите вызов метода, который устанавливает цвет очистки сцены, ее фон /// <summary> /// Инициализирует некоторые атрибуты OpenGL. /// </summary> void InitGL() { // Цвет фона темно синий gl.ClearColor((float)clearColor.R / 255, (float)clearColor.G / 255, (float)clearColor.B / 255, (float)clearColor.A / 255); } 18 Метод BuildFrame будет воспроизводить команды OpenGL, формирующие изображение. Добавьте описание метода BuildFrame к разделу "Методы воспроизведения". Поместите в него команду очистки буфера цвета /// <summary> /// Устанавливает воспроизводимую сцену с помощью команд OpenGL /// </summary> void BuildFrame() { // Очистка буферов gl.Clear(gl.COLOR_BUFFER_BIT); } В тело конструктора формы f3D после создания объекта класса glPort добавьте оператор вызова метода InitGL так, что тело конструктора примет вид public f3D() { InitializeComponent(); // Операторы, делающие форму квадратной со стороной side, // равной минимальной стороне дисплея. // Класс Screen представляет дисплей или несколько дисплеев. // Статическое свойство PrimaryScreen класса Screen возвращает главный дисплей // Свойство Bounds возвращает прямоугольник границ дисплея int side = Math.Min(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); Size = new Size(side, side); port = new glPort(panelGL); InitGL(); } В тело метода RenderFrame() перед командой обмена буферов SwapBuffers добавьте вызов метода BuildFrame() так, что тело метод RenderFrame() примет вид void RenderFrame() { BuildFrame(); if (!port.SwapBuffers()) throw new Exception("Не выполнен обмен буферов."); } Убедитесь, что после этой редакции панель стала темно синей. Пройдите пошагово код проекта, уяснив последовательность выполнения операторов. Тест рубежного контроля 11. Для чего служит описанный класс gl? 12. Какой метод определяет цвет фона изображения? 13. Какой метод очищает буфер цвета, заполняя его заданным фоном? 14. В какой последовательности следует использовать методы ClearColor, Clear, SwapBuffers? Структура буферов. Методы Get В этом разделе Вы узнаете О структуре буферов, хранящих информацию о пикселях экрана О том, как считывать информацию о состоянии OpenGL Информация о каждом пикселе хранится в различных областях памяти – буферах цвета, глубины, трафарета, буфере-аккумуляторе. Каждый буфер имеет стандартную структуру. Он состоит из битовых 19 плоскостей. В каждой битовой плоскости хранится часть информации обо всех пикселях, выводимых на экран. На битовой плоскости столько же ячеек, сколько пикселей на экране. В каждой ячейке битовой плоскости может храниться только единичка или ноль – один бит информации. Отсюда название плоскости. Сколько битовых плоскостей в конкретном буфере, столько битов информации о каждом пикселе содержит буфер. Так, если в буфере две битовые плоскости, то на каждый пиксель приходится два бита информации, т.е. может быть 4 типа пикселей, отличающихся числамииндексами 0, 1, 2, 3. В общем случае в буфере с n битовыми плоскостями каждый пиксель может иметь 2n индексов со значениями от 0 до 2n – 1. К примеру, если в буфере красного цвета (Red) 8 битовых плоскостей, то каждый пиксель может иметь 28 = 256 интенсивностей красного в своем цвете. Метод Get. Получение информации о состоянии OpenGL Ответы на любые вопросы о текущем состоянии настроек OpenGL можно получить, используя метод Get из библиотеки OpenGL. Метод Get позволяет получить, в частности, информацию об объеме всех буферов, на которые рассчитан формат пикселя. Добавьте в класс gl библиотеки GL раздел "Методы Get и их аргументы" со следующим содержанием #region Методы Get и их аргументы // Аргументы метода Get, для определения числа битовых плоскостей // в буферах цвета RED,GREEN,BLUE,ALPHA public const int RED_BITS = 0x0D52; public const int GREEN_BITS = 0x0D53; public const int BLUE_BITS = 0x0D54; public const int ALPHA_BITS = 0x0D55; // Аргумент метода Get, для определения числа битовых плоскостей в буфере глубины public const int DEPTH_BITS = 0x0D56; // Аргумент метода Get, для определения числа битовых плоскостей в буфере трафарета public const int STENCIL_BITS = 0x0D57; // Аргументы метода Get для определения числа битовых плоскостей в буфере-аккумуляторе public const int ACCUM_RED_BITS = 0x0D58; public const int ACCUM_GREEN_BITS = 0x0D59; public const int ACCUM_BLUE_BITS = 0x0D5A; public const int ACCUM_ALPHA_BITS = 0x0D5B; /// <summary> /// Возвращает значения параметра с именем prmName в виде массива типа float. /// </summary> [DllImport("OPENGL32.DLL")] static extern void glGetFloatv(int prmName, float[] parameters); /// <summary> /// Возвращает одно или несколько значений, отвечающих указанному типу параметра. /// </summary> /// <param name="prmName"> /// Символьная постоянная, обозначающая имя параметра, /// значения которого следует возвратить. /// </param> /// <param name="arrayLength"> /// Длина массива возвращаемых значений /// </param> 20 /// <returns> /// Массив значений /// </returns> public static float[] Get(int prmName, int arrayLength) { float[] resultArray = new float[arrayLength]; glGetFloatv(prmName, resultArray); return resultArray; } #endregion Добавьте в метод InitGL формы f3D оператор вывода в строку статуса информации о числе битовых плоскостей буферов цвета, глубины, трафарета и аккумулятора // Сведения о буферах stLabel.Text = stLabel.Text + " Color: Red: " + gl.Get(gl.RED_BITS, 1)[0] + " Green: " + gl.Get(gl.GREEN_BITS, 1)[0] + " Blue: " + gl.Get(gl.BLUE_BITS, 1)[0] + " Alpha: " + gl.Get(gl.ALPHA_BITS, 1)[0] + " Depth: " + gl.Get(gl.DEPTH_BITS, 1)[0] + " Stencil: " + gl.Get(gl.STENCIL_BITS, 1)[0] + " Acc: Red: " + gl.Get(gl.ACCUM_RED_BITS, 1)[0] + " Green: " + gl.Get(gl.ACCUM_GREEN_BITS, 1)[0] + " Blue: " + gl.Get(gl.ACCUM_BLUE_BITS, 1)[0]; Посмотрите результат. Метод GetString. В библиотеке OpenGL есть метод GetString, позволяющий, в частности, получить сведения о версии библиотеки, установленной на компьютере, ее производителе и микросхеме, ответственной за визуализацию команд OpenGL (так называемый Renderer). Добавьте в раздел "Методы Get и их аргументы" класса gl библиотеки GL описание /// <summary> /// Имя объекта-аргумента GetString для определения версии OpenGL. /// </summary> public const int VERSION = 0x1F02; /// <summary> /// Имя объекта-аргумента GetString для определения производителя OpenGL. /// </summary> public const int VENDOR = 0x1F00; /// <summary> /// Имя объекта-аргумента GetString для определения имени визуализатора OpenGL. /// Возвращаемое имя специфично для конкретной аппаратной платформы компьютера. /// </summary> public const int RENDERER = 0x1F01; /// <summary> /// Возвращает указатель на объект с именем name. /// </summary> [DllImport("OPENGL32.DLL")] static extern IntPtr glGetString(int name); 21 /// <summary> /// Возвращает строку с объектом с именем name. /// </summary> public static string GetString(int name) { IntPtr i = glGetString(name); return Marshal.PtrToStringAnsi(i); } Добавьте к предыдущей редакции метода InitGL оператор вывода в строку статуса информации о версии, производителе и визуализаторе так, что метод InitGL примет вид void InitGL() { // Цвет фона синий gl.ClearColor((float)clearColor.R / 255, (float)clearColor.G / 255, (float)clearColor.B / 255, (float)clearColor.A / 255); // Вывод результатов работы методов Get и GetString stLabel.Text += " Color: Red: " + gl.Get(gl.RED_BITS, 1)[0] + " Green: " + gl.Get(gl.GREEN_BITS, 1)[0] + " Blue: " + gl.Get(gl.BLUE_BITS, 1)[0] + " Alpha: " + gl.Get(gl.ALPHA_BITS, 1)[0] + " Depth: " + gl.Get(gl.DEPTH_BITS, 1)[0] + " Stencil: " + gl.Get(gl.STENCIL_BITS, 1)[0] + " Acc: Red: " + gl.Get(gl.ACCUM_RED_BITS, 1)[0] + " Green: " + gl.Get(gl.ACCUM_GREEN_BITS, 1)[0] + " Blue: " + gl.Get(gl.ACCUM_BLUE_BITS, 1)[0] + "; Version: " + gl.GetString(gl.VERSION) + "; Vendor: " + gl.GetString(gl.VENDOR) + "; Renderer: " + gl.GetString(gl.RENDERER); } Две последние строки – имя производителя (Vendor) и тип схемы воспроизведения команд OpenGL (Renderer) являются решающими в определении особенностей работы команд библиотеки OpenGL, установленной на компьютере. Методы GetError и gluErrorString Метод GetError из библиотеки opengl32.dll и метод gluErrorString из библиотеки glu32.dll позволяют получить информацию об ошибке, допущенной при обращении к командам OpenGL или при выполнении этих команд. Для использования этих методов добавьте в раздел "Методы Get и их аргументы" класса gl библиотеки GL описание кодов возможных ошибок в форме констант и описание методов, используемых для регистрации ошибок. Комментарии, дающиеся в описании, позволяют понять смысл этих новых членов класса gl. /// <summary> /// Код отсутствии ошибки. /// Используется как результат вызова GetError. /// </summary> const int NO_ERROR = 0; /// <summary> 22 /// Код ошибки "Недопустимое значение перечислимого аргумента". /// Используется как результат вызова GetError. /// </summary> const int INVALID_ENUM = 0x0500; /// <summary> /// Код ошибки "Численное значение аргумента выходит за границы допустимых значений". /// Используется как результат вызова GetError. /// </summary> const int INVALID_VALUE = 0x0501; /// <summary> /// Код ошибки "Указанная операция недопустима в данном состоянии". /// Используется как результат вызова GetError. /// </summary> const int INVALID_OPERATION = 0x0502; /// <summary> /// Код ошибки "Стек переполнен". /// Используется как результат вызова GetError. /// </summary> const int STACK_OVERFLOW = 0x0503; /// <summary> /// Код ошибки "Недостаточно данных в стеке". /// Используется как результат вызова GetError. /// </summary> const int STACK_UNDERFLOW = 0x0504; /// <summary> /// Код ошибки "Не хватает памяти для выполнения команды". /// Используется как результат вызова GetError. /// </summary> const int OUT_OF_MEMORY = 0x0505; /// <summary> /// Используется для определения ошибки в командах OpenGL. /// </summary> /// <returns> /// Целое число (код ошибки), указывающее на отсутствие или наличие ошибки /// в выполненных командах OpenGL. /// Может иметь одно из 7-ми значений из числа NO_ERROR, INVALID_ENUM, INVALID_VALUE, /// INVALID_OPERATION, STACK_OVERFLOW, STACK_UNDERFLOW, OUT_OF_MEMORY. /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "glGetError")] static extern int GetError(); /// <summary> /// Используется для определения строки, описывающей ошибку в OpenGL. /// </summary> /// <param name="errCode"> /// Код ошибки. /// </param> /// <returns> /// Указатель на строку, описывающую ошибку. /// </returns> 23 [DllImport("GLU32.DLL")] static extern IntPtr gluErrorString(int errCode); /// <summary> /// Используется для определения строки, описывающей ошибку в OpenGL. /// </summary> /// <param name="errCode"> /// Код ошибки. /// </param> /// <returns> /// Строка, описывающая ошибку. /// </returns> public static string getErrorString(int errCode) { return Marshal.PtrToStringAnsi(gluErrorString(errCode)); } /// <summary> /// Порождает объект исключительной ситуации в случае возникновения ошибки /// при выполнении команд OpenGL. /// Сообщение объекта исключительной ситуации содержит описание ошибки. /// </summary> public static void ErrorException() { int error = GetError(); if (NO_ERROR != error) throw new Exception("OpenGL error: " + getErrorString(error)); } Внутри метода RenderFrame() формы f3D (после вызова BuildFrame()) добавьте вызов gl. ErrorException(); Этот вызов будет предохранять код метода BuildeFrame() от возможных ошибок. Посмотрите результат работы приложения. Активируйте для сравнения авторское приложение. Тест рубежного контроля 15. Что такое битовая плоскость буфера? 16. Сколько различных чисел могут определять один и тот же пиксель в буфере, состоящем из 8 битовых плоскостей? 17. Какой метод позволяет возвратить текущие значения параметров OpenGL.? 18. Что делает метод GetString? 19. Для чего служат методы GetError и gluErrorString? Что дальше. Как изобразить одну точку? Что такое примитивы? 24 Примитивы В этом большом разделе вы познакомитесь со всеми примитивами библиотеки OpenGL, их атрибутами и многими командами, управляющими выводом примитивов на экран. Модуль 0. Точки Точка является простейшим примитивом OpenGL. Она обладает рядом атрибутов. Это размер, сглаживаемость, цвет, положение. Комплексной целью модуля является изучение следующих элементов OpenGL Атрибуты точки Цвет, маска цвета Альфа-тестирование Системы координат в OpenGL Тест глубины Дисплейные списки Вывод символов Методы Begin, End. Вершины. Примитив POINTS Построение примитива в OpenGL требует выполнения ряда обязательных команд. К ним относятся "Команды-скобки" Begin и End, внутри которых примитив строится путем перечисления его вершин. Команда Begin имеет параметр, определяющий тип примитива. Команды Vertex, определяющие расположения вершин строящегося примитива. На форме f3D создайте главное меню. Для этого Откройте окно дизайна f3D Войдите в окно ToolBox. Найдите раздел Menus & Toolbars Найдите в нем класс MenuStrip Перенесите объект этого класса на верхнюю панель TopToolStripPanel объекта toolStripContainer1. Дайте ему имя (свойство Name в окне Properties) menuStrip В появившемся окошке названия меню наберите текст Primitives. В окошке ниже наберите Points. Это будет первая команда меню Primitives. В появившемся окошке подменю, справа от Points наберите текст "One point". Это будет первая команда меню "Points". Дальнейшее изложение будет предполагать использование "театральной" терминологии. Каждое меню ( в данном случае это меню с текстом "Primitives") будет соответствовать действию спектакля (action), команда меню (в данном случае "Points") – акту данного действия спектакля (act), и, наконец, команда подменю (в данном случае "One point") будет обозначать сцену спектакля (scene) в данном акте и данном действии. При выборе каждой команды подменю приложение воспроизводит сцену, инициализируя свойства (атрибуты) тех объектов, которые в ней участвуют. На этом этапе в коде формы f3D проведите следующую редакцию Введите раздел "Методы инициализации" и опишите в нем метод InitSceneAttributes, в котором пока лишь обнуляется строка статуса #region Методы инициализации void InitSceneAttributes() 25 { #region Init Common Controls&Fields stLabel.Text = string.Empty; #endregion } #endregion В раздел "Рабочие поля и свойства" введите описание /// <summary> /// Хранит текст выбранной команды подменю (имя сцены) /// </summary> string SceneName; /// <summary> /// Устанавливает и возвращает текущий индекс подменю (номер сцены) /// </summary> public static int SceneNmb { private set; get; } /// <summary> /// Хранит текст выбранной команды меню (имя акта) /// </summary> string ActName; /// <summary> /// Устанавливает и возвращает текущий индекс команды меню (номер акта) /// </summary> public static int ActNmb { private set; get; } /// <summary> /// Хранит текст текущего раздела главного меню (имя действия) /// </summary> string ActionName; /// <summary> /// Устанавливает и возвращает текущий индекс главного меню (номер действия) /// </summary> public static int ActionNmb { private set; get; } В раздел "Методы воспроизведения" добавьте новый метод RenderScene, который устанавливает в заголовок формы имена и номера действия, акта и сцены, вызывает инициализатор сцены InitSceneAttributes и, наконец, вызывает отображение начального фрейма сцены на экране RenderFrame 26 void RenderScene() { Text = ActionName + " (" + ActionNmb.ToString() + "): " + ActName + " (" + ActNmb.ToString() + "): " + SceneName + " (" + SceneNmb.ToString() + ")"; InitSceneAttributes(); RenderFrame(); } Добавьте раздел "Главное меню" и в нем опишите метод, который будет использоваться как обработчик события вызова команд подменю. Операторы обработчика определяют имена и номера действия, акта и сцены и вызывают метод RenderScene #region Главное меню private void subMenuItem_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e) { SceneName = e.ClickedItem.Text; SceneNmb = (sender as ToolStripMenuItem).DropDownItems.IndexOf(e.ClickedItem); ActName = (sender as ToolStripMenuItem).Text; ActNmb = (sender as ToolStripMenuItem).Owner.Items.IndexOf(sender as ToolStripMenuItem); ActionName = (sender as ToolStripMenuItem).OwnerItem.Text; ActionNmb = menuStrip.Items.IndexOf((sender as ToolStripMenuItem).OwnerItem); RenderScene(); } #endregion Присоедините метод subMenuItem_DropDownItemClicked к событию DropDownItemClicked команды "Points" (именно "Points", а не "One point"!!). Для этого o в окне Properties найдите объект pointsToolStripMenuItem o в этом же окне перейдите на страницу событий (значок молнии) и найдите событие DropDownItemClicked o добавьте из выпадающего списка обработчик subMenuItem_DropDownItemClicked. Добавьте в раздел "Методы воспроизведения" описание нового, пока пустого метода с именем SetCurPointsActAttr: void SetCurPointsActAttr() { } Добавьте в конец тела метода BuildFrame формы f3D оператор // Построение изображения switch (ActName) { case "Points": #region Points SetCurPointsActAttr(); #region Puppets switch (SceneName) { case "One point": //0 break; } #endregion #endregion 27 break; } В разделе (#region) "Points" будут находиться операторы, строящие изображения сцен из акта "Points". В метод SetCurPointsActAttr (пока пустой) будут помещаться операторы, устанавливающие текущие параметры участников акта "Points" в зависимости от сцены. Будем считать спектакль кукольным и назовем участников спектакля куклами, или марионетками (puppets). В раздел "Puppets" в зависимости от сцены будут помещаться операторы, строящие изображение куклы – геометрического объекта, воспроизводимого на экране. Основой построения примитивов в OpenGL является так называемая вершина (vertex). Под вершинами понимаются именно вершины простых геометрических фигур – треугольника, четырехугольника, многоугольника. У отрезка прямой – две вершины, у точки – одна. Вызывать методы Vertex можно только между двумя последовательными вызовами методов Begin и End. При этом аргумент метода Begin определяет, к какому типу примитивов вызываемая вершина относится. Внимательно ознакомьтесь с комментариями в описании этих методов. Перейдите к файлу библиотеки GL.cs и в класс gl добавьте описание примитивов и методов, с помощью которых они изображаются #region Постоянные примитивов /// <summary> /// Рассматривает каждую вершину, как отдельную точку /// </summary> public const int POINTS = 0x0000; /// <summary> /// Рассматривает каждую пару вершин как независимый отрезок прямой. /// Вершины 2n - 1 и 2n определяют прямую n. /// Изображается N/2 прямых, где N - полное число вершин. /// </summary> public const int LINES = 0x0001; /// <summary> /// Изображает связанную группу отрезков прямых от первой до последней вершины, /// затем назад - к первой вершине. /// Вершины n и n+1 определяют прямую n. Последняя прямая определяется вершинами N и 1. /// Изображается N прямых, где N - полное число вершин. /// </summary> public const int LINE_LOOP = 0x0002; /// <summary> /// Изображает связанную группу отрезков прямых от первой до последней вершины. /// Вершины n и n+1 определяют прямую n. /// Изображается N - 1 прямая, где N - полное число вершин. /// </summary> public const int LINE_STRIP = 0x0003; /// <summary> /// Рассматривает каждый триплет вершин, как независимый треугольник. /// Вершины 3n - 2, 3n-1 и 3n определяют треугольник n. /// Изображается N/3 треугольников, где N - полное число вершин. /// </summary> public const int TRIANGLES = 0x0004; /// <summary> /// Изображает связанную группу треугольников. 28 /// Один треугольник определен для каждой вершины после первых двух вершин. /// Для нечетных n, вершины n, n+1 и n+2 определяют треугольник n. /// Для четных n, вершины n+1, n и n+2 определяют треугольник n. /// Изображается N - 2 треугольника, где N - полное число вершин. /// </summary> public const int TRIANGLE_STRIP = 0x0005; /// <summary> /// Изображает связанную группу треугольников. /// Один треугольник определен для каждой вершины после первых двух вершин. /// Вершины 1, n+1 и n+2 определяют треугольник n. /// Изображается N - 2 треугольника, где N - полное число вершин. /// </summary> public const int TRIANGLE_FAN = 0x0006; /// <summary> /// Рассматривает каждую группу из четырех вершин, как независимый четырехугольник. /// Вершины 4n - 3, 4n - 2, 4n - 1 и 4n определяют четырехугольник n. /// Изображается N/4 четырехугольника, где N - полное число вершин. /// </summary> public const int QUADS = 0x0007; /// <summary> /// Изображает связанную группу четырехугольников. /// Один четырехугольник определен для каждой пары вершин после первой пары. /// Вершины 2n - 1, 2n, 2n+2 и 2n+1 определяют четырехугольник n. /// Изображается N четырехугольника, где N - полное число вершин. /// Заметьте, что порядок используемых вершин /// для построения четырехугольника в данном примитиве /// отличается от того, что используется при построении независимых четырехугольников. /// </summary> public const int QUAD_STRIP = 0x0008; /// <summary> /// Изображает один выпуклый многоугольник. /// Вершины от 1 до N определяют этот многоугольник. /// </summary> public const int POLYGON = 0x0009; #endregion #region Методы Begin, End, Vertex /// <summary> /// Begin и End ограничивают вершины примитива или группы примитивов одного типа /// </summary> /// <param name="mode"> /// Определяет примитив или примитивы, которые будут созданы из вершин, представленных /// между вызовом Begin и последующим вызовом End. /// В качестве значений доступны десять символических постоянных: /// POINTS, LINES, LINE_STRIP, LINE_LOOP, TRIANGLES, /// TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP и POLYGON. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glBegin")] public static extern void Begin(int mode); /// <summary> 29 /// Begin и End ограничивают вершины примитива или группы примитивов одного типа. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glEnd")] public static extern void End(); /// <summary> /// Методы Vertex используются внутри пары Begin/End /// для определения положения вершин точки, линии и многоугольника. /// Текущий цвет, направление нормали и координаты текстуры ассоциируются с вершиной, /// когда вызывается Vertex. /// Если заданы только две координаты, то это x и y, при этом z по умолчанию равна 0.0 /// и четвертая координата (масштаб) w по умолчанию равна 1.0. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glVertex2f")] public static extern void Vertex(float x, float y); /// <summary> /// Методы Vertex используются внутри пары Begin/End /// для определения положения вершин точки, линии и многоугольника. /// Текущий цвет, направление нормали и координаты текстуры ассоциируются с вершиной, /// когда вызывается Vertex. /// Если заданы три координаты x, y и z, то координата w по умолчанию равна 1.0. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glVertex3f")] public static extern void Vertex(float x, float y, float z); /// <summary> /// Методы Vertex используются внутри пары Begin/End /// для определения положения вершин точки, линии и многоугольника. /// Текущий цвет, направление нормали и координаты текстуры ассоциируются с вершиной, /// когда вызывается Vertex. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glVertex4f")] public static extern void Vertex(float x, float y, float z, float w); #endregion Вернувшись к методу BuildFrame формы f3D, вставьте в раздел "Points" операторы, позволяющие изобразить одну точку. Так, что весь метод BuildeFrame примет вид void BuildFrame() { // Очистка буфера цвета gl.Clear(gl.COLOR_BUFFER_BIT); // Построение изображения switch (ActName) { case "Points": #region Points SetCurPointsActAttr(); #region Puppets switch (SceneName) { case "One point": //0 30 gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); break; } #endregion #endregion break; } } Активируйте проект и убедитесь, что в центре панели появилась белая точка. Результат должен быть такой же, как здесь. Тест рубежного контроля 1. Что такое примитив в терминологии OpenGL? 2. Для чего служат методы Begin, End? 3. Какой аргумент должен принимать метод Begin для изображения точек? 4. Что делает метод Vertex? 5. Какие версии метода Vertex описаны в этом разделе? Что дальше. Точка может иметь размеры, цвет, изображаться в форме квадрата или круга. Как регулировать эти атрибуты? 1 Атрибуты точки По умолчанию примитив точка изображается в форме белого квадрата со стороной в один пиксель. В OpenGL существует возможность менять размеры, форму и цвет точки. В этом разделе будут описаны методы, которые Управляют размерами точки Определяют внешний вид точки Позволяют сохранить предыдущие установки атрибутов, изменив их на время выполнения отдельных действий. Определяют цвет вершины Позволяют регулировать вывод компонент цвета на экран Метод PointSize Обычно, по умолчанию, размер точки равен единице. Метод void PointSize(float size) позволяет задавать диаметр size растра изображаемой точки. Вот его описание, которое следует поместить в класс gl библиотеки GL /// <summary> /// Определяет диаметр растра изображаемой точки. /// </summary> /// <param name="size"> /// Диаметр от некоторого минимального до некоторого максимального значения. /// По умолчанию диаметр обычно равен 1. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glPointSize")] public static extern void PointSize(float size); Вызов метода PointSize внутри скобок Begin/End приведет к ошибке. 31 Для примера использования метода PointSize поместите его вызов внутрь метода BuildeFrame() формы f3D , в случай "One point" так, чтобы новая версия кода в этом случае выглядела следующим образом case "One point": //0 gl.PointSize(10); gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); break; Точка должна выглядеть как большой квадрат. Измените значения параметра 10, выбираемого для PointSize. Посмотрите результат. Текущее значение диаметра size может быть определено вызовом метода Get в форме gl.Get(gl.POINT_SIZE, 1)[0], где в качестве первого аргумента используется константа /// <summary> /// Аргумент Get требует возврата значения одного параметра - размера точки /// </summary> public const int POINT_SIZE = 0x0B11; Размер точки может меняться в определенных пределах, определяемых методом Get в форме Для получения минимального размера точки gl.Get(gl.POINT_SIZE_RANGE, 2)[0] Для получения максимального размера точки gl.Get(gl.POINT_SIZE_RANGE, 2)[1] Видимый размер точки меняется с определенным шагом (granularity), который возвращается функцией gl.Get(gl.POINT_SIZE_GRANULARITY, 1)[0]. Постоянные, с которыми вызываются эти функции /// <summary> /// Аргумент Get требует возврата значения двух параметров /// минимального и максимально возможного размеров точки. /// </summary> public const int POINT_SIZE_RANGE = 0x0B12; /// <summary> /// Аргумент Get требует возврата значения одного параметра – /// различия в значениях размеров точки, /// которые учитываются в изображении. /// </summary> public const int POINT_SIZE_GRANULARITY = 0x0B13; Сглаживание. Методы Enable, Disable и IsEnable Другим атрибутом изображения точки является сглаживание ("smooth" antialiasing mechanism). При включенном сглаживании точка изображается не квадратом, а кругом, размеры которого попрежнему определяются методом PointSize. Включение сглаживания (как и многих других параметров в OpenGL) выполняется с помощью метода /// <summary> /// Активирует режимы формирования изображения командами OpenGL /// </summary> /// <param name="capability"> /// Символьная постоянная, указывающая активируемый режим. 32 /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glEnable")] public static extern void Enable(int capability); а отключение - его антиподом /// <summary> /// Дезактивирует режимы формирования изображения командами OpenGL /// </summary> /// <param name="capability"> /// Символьная константа, указывающая дезактивируемый режим. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glDisable")] public static extern void Disable(int capability); Конкретно, для включения эффекта сглаживания следует использовать метод Enable с аргументом, определенным константой /// <summary> /// Параметр сглаживания изображения точки. /// Используется методами Get, Enable, Disable и IsEnable. /// </summary> public const int POINT_SMOOTH = 0x0B10; Для отключения сглаживания используется метод Disable с тем же аргументом. Поместите приведенные описания методов Enable, Disable и постоянной POINT_SMOOTH в класс gl библиотеки GL. В методе BuildeFrame() формы f3D, в том же случае "One point" измените код, чтобы проверить действие эффекта сглаживания case "One point": //0 gl.PointSize(10); gl.Enable(gl.POINT_SMOOTH); gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); break; Точка должна приобрести вид круга. Для определения, включено ли сглаживание, можно использовать метод Get с тем же аргументом в форме gl.Get(gl.POINT_SMOOTH, 1)[0] != 0, либо метод (который следует добавить в класс gl библиотеки GL) /// <summary> /// Определяет, активирован ли режим /// </summary> /// <param name="capability"> /// Символьная постоянная, указывающая дезактивируемый режим. /// </param> /// <returns> /// true или false, в зависимости от результатов проверки /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "glIsEnabled")] public static extern bool IsEnabled(int capability); с тем же аргументом POINT_SMOOTH. 33 Методы PushAttrib, PopAttrib OpenGL имеет практичный способ сохранять нетронутыми ранее установленные атрибуты при их временном изменении. Все атрибуты, регулирующие свойства изображения, формируемые командами OpenGL, и активность многих функций этой библиотеки, содержатся в специальном стеке памяти. Если необходимо временно изменить часть атрибутов, как, например, размер точки, можно создать копию прежних значений, "затолкнув" их в стек (push). В образовавшейся копии наверху стека можно менять атрибуты, строя новые образы. После окончания создания новых образов, измененные атрибуты, в которых больше нет необходимости, можно вытолкнуть (pop) из стека. На их место вернется сохраненная копия прежних атрибутов. Для работы механизма сохранения и восстановления атрибутов существуют методы /// <summary> /// Заталкивает в стек копию атрибутов, определяемых аргументом mask. /// </summary> /// <param name="mask"> /// Символьная константа, определяющая атрибуты, копируемые в стек. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glPushAttrib")] public static extern void PushAttrib(int mask); /// <summary> /// Восстанавливает сохраненную копию атрибутов из стека. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glPopAttrib")] public static extern void PopAttrib(); В частности, аргументом метода PushAttrib при работе с такими атрибутами точки как размеры и сглаживание должна быть константа /// <summary> /// Маска атрибутов точки. /// </summary> public const int POINT_BIT = 0x00000002; Поместите описания методов PushAttrib, PopAttrib и постоянной POINT_BIT в класс gl библиотеки GL. Проверьте их работу, изменив код случая "One point" метода BuildFrame() в форме f3D. Например, замените прежний код новым кодом вида case "One point": //0 gl.PointSize(10); gl.Enable(gl.POINT_SMOOTH); gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); // Заталкиваем в стек установленный размер точки и включенный эффект сглаживания gl.PushAttrib(gl.POINT_BIT); // Строим точку, сдвинутую вправо по оси x, // с новым размером (5) и без эффекта сглаживания gl.PointSize(5); gl.Disable(gl.POINT_SMOOTH); gl.Begin(gl.POINTS); gl.Vertex(.5f, 0); gl.End(); 34 // Выталкиваем из стека временно установленные атрибуты точки, // возвращаясь к прежним атрибутам gl.PopAttrib(); // Изображаем точку, сдвинуту влево по оси x, // с прежними атрибутами: размер 10, есть эффект сглаживания gl.Begin(gl.POINTS); gl.Vertex(-.5f, 0); gl.End(); break; В результате на экране должны появиться три точки – в центре размером 10 и в форме круга, справа размером 5 и в форме квадрата, и, наконец, слева опять размером 10 в форме круга. Цвет вершины. Метод Color Цвет точки по умолчанию белый. Его можно сделать произвольным с помощью команд Color библиотеки OpenGL. Так выглядит описание методов Color /// <summary> /// Устанавливает текущий цвет. Может иметь три или 4 параметра типа float. /// Первые три параметра определяют интенсивности красного, зеленого и синего цветов /// и лежат в интервале от 0 (нулевая интенсивность) до 1 (полная интенсивность). /// Четвертый параметр определяет интенсивность альфа-компоненты, /// или "компоненты прозрачности" в тех же пределах. /// Если альфа-компонента не задается, то ее значение равно 1 (полная интенсивность). /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glColor3f")] public static extern void Color(float red, float green, float blue); [DllImport("OPENGL32.DLL", EntryPoint = "glColor4f")] public static extern void Color(float red, float green, float blue, float alpha); Каждая вершина может иметь свой цвет, если предварить вызов метода Vertex вызовом метода Color. Поместите эти методы в класс gl библиотеки GL. Для проверки работы метода Color внесите изменение в код случая "One point" в методе BuildFrame() класса f3D. Например, напишите этот случай в виде case "One point": //0 gl.PointSize(10); gl.Enable(gl.POINT_SMOOTH); gl.Begin(gl.POINTS); gl.Color(1, 0, 0); gl.Vertex(0, 0); gl.End(); break; В центре должна появиться круглая красная точка. Узнать текущий цвет можно с помощью метода Get, вызванного в форме gl.Get(gl.CURRENT_COLOR, 4)[i]. Здесь значения i = 0, 1, 2, 3 соответствуют красной, зеленой, синей и альфа-компоненте цвета. Константа CURRENT_COLOR имеет вид public const int CURRENT_COLOR = 0x0B00; 35 При использовании метода PushAttrib для сохранения предыдущего цвета следует использовать аргумент /// <summary> /// Маска атрибутов цвета /// </summary> public const int CURRENT_BIT = 0x00000001; Поместите описание этой постоянной в класс gl библиотеки GL. По аналогии с примером из предыдущего раздела постройте три точки красного и зеленого цветов, используя метод PushAttrib с параметром CURRENT_BIT и метод PopAttrib(). Фильтр цвета. Метод ColorMask В библиотеке OpenGL существует метод ColorMask, маскирующий вывод на экран произвольной компоненты цвета. Добавьте к разделу "Очистка буферов" класса gl библиотеки GL описание /// <summary> /// Маскирует воспроизведение компонент цвета red, green, blue и alpha. /// По умолчанию значения всех параметров true. ///Значение false маскирует соответствующую компоненту. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glColorMask")] public static extern void ColorMask(bool red, bool green, bool blue, bool alpha); В качестве примера использования метода ColorMask в методе BuildFrame() класса формы f3D случай "One point" запишите в виде case "One point": gl.PointSize(10); gl.Enable(gl.POINT_SMOOTH); gl.ColorMask(false, true, true, true); // цвет белый gl.Color(1, 1, 1); // маскировка красного делает белую точку бирюзовой gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); break; Убедитесь, меняя параметры ColorMask, что маскировка убирает любую из цветовых компонент. Точка в центре панели будет изображена бирюзовым кругом, т.к. из ее белого цвета будет убрана красная составляющая. Для определения текущего состояния ColorMask используется метод Get в форме gl.Get(gl.COLOR_WRITEMASK, 4)[i] != 0 Константа COLOR_WRITEMASK имеет вид public const int COLOR_WRITEMASK = 0x0C23; При использовании PushAttrib для изменения ColorMask аргументом должна быть константа COLOR_BUFFER_BIT, уже описанная в классе gl библиотеки GL. Тест рубежного контроля 6. Какой метод регулирует размер точек? 7. Как определить пределы возможных значений размеров точки? 8. Как определить шаг изменения размеров точки? 36 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. Какой метод "включает" сглаживание? Какая постоянная должна быть аргументом метода, включающего сглаживание? Что делает метод Disable? Какой метод определяет активность данного состояния OpenGL? Какой метод сохраняет текущие атрибуты в стеке? Какой аргумент используется при сохранении в стеке атрибутов точки? Что делает метод PopAttrib? Что делает метод Color? Какие версии метода Color используются в проекте? Как определить текущий цвет? Как сохранить в стеке текущий цвет? Какой метод используется для фильтрации цвета? Как узнать текущие значения аргументов метода фильтрации цвета? Как сохранить атрибуты метода ColorMask в стеке? Далее читатель может 1. Посмотреть новый раздел авторской версии приложения, иллюстрирующего работу команд OpenGL. 2. Самостоятельно внести изменения в имеющийся проект, чтобы освоить работу описанных методов. 3. Приступить к созданию авторской версии приложения. В следующем разделе приводятся команды OpenGL, использующие значения 4-ой "невидимой" компоненты цвета – альфа-компоненты. 2 Альфа-тест. Метод AlphaFunc В этом разделе рассмотрены команды, позволяющие фильтровать вывод вершин в буфер фрейма в зависимости от значения альфа-компоненты цвета. Существует способ фильтровать вывод изображения на экран в зависимости от значения параметра alpha метода Color. Это так называемый альфа-тест. Для определения условий альфа-теста используется метод AlphaFunc, задающий функцию тестирования. /// <summary> /// Определяет способ тестирования по параметру альфа. /// Тот или иной способ позволяет фильтровать изображение вершин в зависимости /// от значения альфа-компоненты цвета. /// </summary> /// <param name="function"> /// Определяет функцию сравнения при использовании альфа-тестирования. /// Принимаются символьные постоянные NEVER, LESS, EQUAL, LEQUAL, /// GREATER, NOTEQUAL, GEQUAL и ALWAYS. /// По умолчанию условие альфа-тестирования установлено в ALWAYS, /// то есть никакой фильтрации изображения по параметру альфа не происходит. /// </param> /// <param name="referenceValue"> /// Задает значение параметра альфа, с которым сравнивается другое значение альфа, /// поступающее в буфер вместе с изображением, например, является параметром метода Color. /// Значение параметра сжимается в интервал [0; 1]. Значение по умолчанию равно 0. /// </param> 37 [DllImport("OPENGL32.DLL", EntryPoint = "glAlphaFunc")] public static extern void AlphaFunc(int function, float referenceValue); Использование этого метода требует наличие констант // Символьные постоянные, используемые в определении функции альфа-тестирования public const int NEVER = 0x0200; public const int LESS = 0x0201; public const int EQUAL = 0x0202; public const int LEQUAL = 0x0203; public const int GREATER = 0x0204; public const int NOTEQUAL = 0x0205; public const int GEQUAL = 0x0206; public const int ALWAYS = 0x0207; После задания условий альфа-тестирования необходимо активировать сам процесс тестирования методом Enable с параметром ALPHA_TEST. /// <summary> /// Параметр активации и дезактивации альфа-теста /// </summary> public const int ALPHA_TEST = 0x0BC0; Приведенные описания следует поместить в класс gl библиотеки GL. К примеру, измените код в случае "One point" метода BuildFrame() класса f3D с тем, чтобы увидеть эффект альфа-тестирования case "One point": gl.PointSize(10); // Задается правило тестирования: alpha должно быть меньше 0.2. gl.AlphaFunc(gl.LESS, .2f); // Активируется альфа-тестирование gl.Enable(gl.ALPHA_TEST); gl.Color(1, 0, 0, .1f); // Точка изображается, т.к. альфа-компонента цвета 0.1 меньше, чем заданное выше значение 0.2 gl.Begin(gl.POINTS); gl.Vertex(0, 0); gl.End(); break; Измените в этом коде условие тестирования. Например, замените gl.LESS на gl.GREATER, или 0.2 на 0.05. В обоих случаях точка не будет изображаться, т.к. условия теста не будут соблюдаться. Для определения состояния включения альфа-теста можно использовать метод IsEnabled с тем же параметром ALPHA_TEST, либо метод Get. Для определения текущего значения параметров метода AlphaFunc используется метода Get в форме (int)gl.Get(gl.ALPHA_TEST_FUNC, 1)[0] gl.Get(gl.ALPHA_TEST_REF, 1)[0] Константы, используемые здесь, имеют вид /// <summary> /// Аргумент Get требует возврата значения одного параметра - целочисленной постоянной, /// определяющей функцию альфа-теста. /// </summary> public const int ALPHA_TEST_FUNC = 0x0BC1; /// <summary> 38 /// Аргумент Get требует возврата значения одного параметра /// текущего значения параметра referenceValue метода AlphaFunc. /// </summary> public const int ALPHA_TEST_REF = 0x0BC2; Метод PushAttrib с параметром COLOR_BUFFER_BIT позволяет сохранить в стеке текущие установки альфа-теста. Тест рубежного контроля 23. Как включить альфа-тест? 24. Какой метод определяет правило альфа-тестирования? 25. Какой смысл имеет аргумент GEQUAL метода AlphaFunc? 26. Как узнать текущее состояние активности альфа-тестирования? 27. Как узнать текущие значения аргументов метода AlphaFunc? 28. Как сохранить в стеке текущее состояние альфа-тестирования? Далее читатель может 1. Посмотреть новый раздел авторской версии приложения, иллюстрирующего работу команд OpenGL. 2. Самостоятельно внести изменения в имеющийся проект, чтобы освоить работу описанных методов. 3. Продолжить создание авторской версии приложения, следуя его советам. 3 Объектные координаты вершин В этом разделе будут меняться координаты точки. К чему это приведет? Что такое объектные координаты? Что означает 4-ая координата вершины? До сих пор аргументами метода Vertex были 0,0. Это значения xo и yo – так называемых объектных координат (их еще называют мировыми координатами). Аргументами функции Vertex являются объектные координаты вершины. Точка изображалась в центре панели. Если изменить значение xo = 1, то точка окажется справа на краю панели, значение yo = 1 перенесет точку на верхнюю границу. Внесите эти изменения в свой проект и проверьте, либо войдите в авторский проект и проводите изменения координат вершины с помощью управляющих элементов левой панели. Код данной сцены авторского проекта здесь. У вершины может быть 4-ая координата (третья координата z пока будет равна нулю). Вызовите метод Vertex в версиях с четырьмя координатами Vertex(2,0,0,2), Vertex(0,2,0,2). Убедитесь, что в этих случаях точка будет изображена на правой и верхней границе панели. Дело в том, что на конечном этапе определения места положения точки в окне, значения координат x, y, z делятся на координату w, или, как говорят, нормализуются. Отсюда должна быть понятна роль 4-ой координаты w. Эта координата (по умолчанию равная 1) используется для указания масштаба объектных координат. В приведенных примерах третья координата z, определяющая глубину изображения, была равна нулю. Если метод Vertex вызывается с двумя параметрами, то z = 0 по умолчанию. Придайте z любое значение, отличное от нуля. Убедитесь, что пока значение z лежит в интервале [–w;w] точка будет видна на панели. Тест рубежного контроля 29. Что такое объектные координаты? 30. Каковы пределы изменения координат xo, yo, zo по умолчанию? 31. Что определяет координата w? 32. Что произойдет, если выбрать w = 0? 39 Верните вершину в центр панели. Меняйте размеры окна, сжимая и расширяя его в разных направлениях. Точка потеряет свое центральное положение и будет воспроизводиться в разных местах относительно границ панели в зависимости от ее размеров. Что это означает? 4 Оконные координаты. Порт наблюдения. Метод Viewport Какие преобразования проводит OpenGL с объектными координатами, прежде чем они оказываются оконными координатами? Перед воспроизведением на экране объектные координаты xo, yo, zo, wo, указываемые в качестве параметров метода Vertex, претерпевают ряд линейных преобразований. Вначале они преобразуются в так называемые координаты наблюдения xe, ye, ze, we. Далее, координаты наблюдения преобразуются в координаты отсечения xc, yc, zc, wc. Координаты отсечения делятся на значение wc, образовав нормализованные координаты устройства xd, yd, zd. Под "устройством" понимается "порт наблюдения" – прямоугольник в окне, с которым связан контекст воспроизведения. В данном случае этим окном является панель panelGL. По умолчанию матрицы преобразования от объектных координат к координатам наблюдения и от координат наблюдения к координатам отсечения являются единичными. Поэтому, по умолчанию xo = xc, yo = yc, zo = zc, wo = wc. Следовательно, если координата wo = 1, то объектные координаты xo, yo, zo совпадают с нормализованными координатами устройства xd, yd, zd. Координаты пикселя относительно границ панели xw, yw, zw называются оконными. Оконные координаты xw, yw измеряются в пикселях относительно нижнего левого угла клиентской области окна (в данном случае панели panelGL). Портом наблюдения (Viewport) называют прямоугольную область, выделенную в оконных координатах, в границах которой воспроизводятся объекты OpenGL. В общем случае порт наблюдения определяется положением своего нижнего левого угла (x, y) в оконных координатах относительно левого нижнего угла окна, шириной w и высотой h, выраженных в пикселях. Поэтому положение центра порта ox, oy относительно левого нижнего угла окна имеет вид ox = x + w/2; oy = y + h/2. Между оконными xw, yw и координатами устройства xd, yd (координаты z будут рассматриваться позже) существуют соотношения вида xw = (w/2)*xd + ox; yw = (h/2)*yd + oy. По умолчанию порт наблюдения совпадает со всей клиентской областью окна, то есть, в данном случае, занимает всю поверхность панели panelGL. Поэтому в последних формулах x = 0, y = 0, h высота клиентской области панели, w – ширина этой области. Отсюда следует, что точка с координатами xo = 0, yo = 0 должна по умолчанию получить оконные координаты xw = ox = w/2, yw = oy = h/2. Поэтому точка изображается в центре панели. Однако, изменение размеров панели не влечет за собой автоматического изменения размеров порта наблюдения w, h, фигурирующих в формулах. В OpenGL существует специальная команда Viewport, задающая параметры порта наблюдения. /// <summary> /// Устанавливает порт наблюдения. /// </summary> /// <param name="x"> /// Определяет левую сторону прямоугольника порта наблюдения /// по отношению к левой стороне окна. /// По умолчанию 0. /// </param> /// <param name="y"> /// Определяет нижнюю сторону прямоугольника порта наблюдения 40 /// по отношению к нижней стороне окна. По умолчанию 0. /// </param> /// <param name="width"> /// Определяет ширину порта наблюдения в пикселях. /// Когда контекст воспроизведения впервые привязывается /// к окну значение равно ширине окна. /// </param> /// <param name="height"> /// Определяет высоту порта наблюдения в пикселях. /// Когда контекст воспроизведения впервые привязывается /// к окну значение равно высоте окна. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glViewport")] public static extern void Viewport(int x, int y, int width, int height); Поместите это описание в класс gl библиотеки GL. Можно связать изменение размеров панели с размерами порта наблюдения. Для этого на форме f3D выберите панель panelGL и в окне свойств этой панели на странице обработчиков (кнопка с изображением молнии) найдите обработчик события SizeChanged. Этот обработчик будет выполняться при изменении размеров панели panelGL. Поместите в обработчик события SizeChanged панели panelGL код // Если контекст воспроизведения не установлен, то выход из метода if (null == port) return; gl.Viewport(0, 0, panelGL.ClientSize.Width, panelGL.ClientSize.Height); RenderFrame(); Теперь изменения размеров панели не будет влиять на положение точки, так как порт наблюдения будет автоматически подстраиваться под размеры панели. Порт наблюдения всегда будет совпадать с клиентской областью панели panelGL и точка с объектными координатами 0, 0 окажется в центре панели. Метод Project В OpenGL существует команда Project, которая позволяет по объектным координатам вершины определить оконные координаты /// <summary> /// Отображает объектные координат objx, objy, objz /// на оконные координаты winx, winy, winz при заданных: /// матрице преобразования от объектных к координатам наблюдения modelMatrix, /// матрице преобразования projMatrix от координат наблюдения к координатам отсечения /// и параметрах порта наблюдения viewport /// </summary> [DllImport("GLU32.DLL", EntryPoint = "gluProject")] public static extern void Project(double objx, double objy, double objz, double[] modelMatrix, double[] projMatrix, int[] viewport, out double winx, out double winy, out double winz); Поместите это описание метода Project в класс gl библиотеки GL. Для использования этого метода необходимо знать текущую матрицу преобразования от объектных координат к координатам наблюдения (MODELVIEW_MATRIX), от координат наблюдения к координатам отсечения (PROJECTION_MATRIX), и, наконец, параметры порта наблюдения (VIEWPORT). Все эти величины можно получить, используя функцию Get в форме 41 double[] prMtrx = new double[16], mvMtrx = new double[16]; int[] vp = new int[4]; for (int i = 0; i < 16; i++) { prMtrx[i] = gl.Get(gl.PROJECTION_MATRIX, 16)[i]; mvMtrx[i] = gl.Get(gl.MODELVIEW_MATRIX, 16)[i]; if (i < 4) vp[i] = (int)gl.Get(gl.VIEWPORT, 4)[i]; } Константы, которые использует Get в данном случае, имеют вид /// <summary> /// Аргумент Get требует возврата значения 16 параметров матрицы преобразования /// от объектных координат к координатам наблюдения, находящейся на верху стека. /// </summary> public const int MODELVIEW_MATRIX = 0x0BA6; /// <summary> /// Аргумент Get требует возврата значения 16 параметров матрицы преобразования /// от координат наблюдения к координатам отсечения, находящейся на верху стека. /// </summary> public const int PROJECTION_MATRIX = 0x0BA7; /// <summary> /// Аргумент Get требует возврата значения четырех параметров порта наблюдения /// x, y, width, height. /// </summary> public const int VIEWPORT = 0x0BA2; Описание констант поместите в класс gl библиотеки GL. Чтобы наблюдать значения оконных координат вершины, которые возвращает метод Project, добавьте к коду оператора case "One point": код вывода этих значений в строку статуса. Так, что полный код оператора case будет иметь вид case "One point": double[] prMtrx = new double[16], mvMtrx = new double[16]; int[] vp = new int[4]; for (int i = 0; i < 16; i++) { prMtrx[i] = gl.Get(gl.PROJECTION_MATRIX, 16)[i]; mvMtrx[i] = gl.Get(gl.MODELVIEW_MATRIX, 16)[i]; if (i < 4) vp[i] = (int)gl.Get(gl.VIEWPORT, 4)[i]; } float Xo, Yo, Zo; Xo = 0; Yo = 0; Zo = 0; gl.Begin(gl.POINTS); gl.Vertex(Xo, Yo); gl.End(); double Xw, Yw, Zw; gl.Project(Xo, Yo, Zo, mvMtrx, prMtrx, vp, out Xw, out Yw, out Zw); stLabel.Text = string.Format( "Xw={0:f}; Yw={1:f}; panelGL: Width={2}; Height={3}; Центр: Ox={4}; Oy={5}", Xw, Yw, panelGL.ClientSize.Width, panelGL.ClientSize.Height, panelGL.ClientSize.Width / 2, panelGL.ClientSize.Height / 2); break; 42 В результате в строке статуса должна появиться информация, с помощью которой можно проверить формулы преобразования от объектных координат xo, yo к оконным координатам xw, yw: xw = (w/2)*xd + ox; yw = (h/2)*yd + oy. Читатель может отредактировать свой проект, либо посмотреть иллюстрирующее приложение. Ссылка для того, кто продолжает создавать авторское приложение. Посмотрите и проанализируйте результат, меняя положение вершины на панели panelGL. Если возникает необходимость сохранить параметры порта наблюдения в стеке, то используется метод PushAttrib с параметром VIEWPORT. Тест рубежного контроля 33. Какие системы координат используют команды OpenGL? 34. Что такое порт наблюдения? 35. Что такое оконные координаты? 36. Как определены параметры порта наблюдения по умолчанию? 37. Какой метод управляет параметрами порта наблюдения? 38. Где следует вызывать метод, определяющий порт наблюдения? 39. Что делает метод Project? 40. Как определить текущие матрицы преобразований между различными системами координат и параметры порта наблюдения? 5 Тест глубины В разделе рассматриваются методы OpenGL, обеспечивающие эффект глубины в зависимости от значений zкоординаты вершины. Это, так называемый "тест глубины", включив и настроив который, можно различать положение объектов по глубине – расстоянию от наблюдателя до объекта по нормали к плоскости экрана. Включается тест глубины методом Enable с параметром /// <summary> /// Параметр активации теста глубины. /// </summary> public const int DEPTH_TEST = 0x0B71; Узнать, включен ли тест глубины можно методом IsEnabled либо методом Get с тем же параметром. При работе с буфером глубины необходимо в начале каждого фрейма очищать этот буфер точно так же, как это делается с буфером цвета. Для очистки используется тот же метод Clear с параметром /// <summary> /// Идентифицирует буфер глубины в методах Clear и PushAttrib. /// </summary> public const int DEPTH_BUFFER_BIT = 0x00000100; Тот же параметр используется для сохранения состояния буфера глубины в стеке методом PushAttrib. Поместите описания этих констант в класс gl библиотеки GL. Для проверки работы теста глубины необходимы две точки, имеющие одни и те же координаты xo, yo, но разные координаты zo. Преобразование порта наблюдения координаты zo (которая в данном случае w = 1 совпадает с zd) имеет вид zw = (f – n)/2*zd + (f + n)/2, где n и f – близкая (near) и дальняя (far) границы буфера глубины. По умолчанию n = 0 и f = 1. Из этой формулы следует, что минимальному значению координаты zo = zd = -1 отвечает zw = n = 0, а максимальному zo = zd = 1 значение zw = f =1. Тестированию подвергаются значения zw. По умолчанию при включении теста глубины более удаленный от наблюдателя пиксель (с большим значением zw) оказывается скрытым более близким (с меньшим значением zw). 43 Если тест глубины не активирован, то из двух вершин, имеющих одни и те же координаты xo, yo будет изображена та, которая направляется в буфер последней по очереди. Если тест включен, то, по умолчанию, будет изображена та, у которой значение zw меньше. Для проверки работы теста глубины следует изменить код в случае "One point" в методе BuildFrame() класса f3D так, чтобы код содержал две точки, имеющие одинаковые значения координат x, y, но разные значения координаты z, и точки имели бы разный цвет. Например, case "One point": //0 double[] prMtrx = new double[16], mvMtrx = new double[16]; int[] vp = new int[4]; for (int i = 0; i < 16; i++) { prMtrx[i] = gl.Get(gl.PROJECTION_MATRIX, 16)[i]; mvMtrx[i] = gl.Get(gl.MODELVIEW_MATRIX, 16)[i]; if (i < 4) vp[i] = (int)gl.Get(gl.VIEWPORT, 4)[i]; } float Zo1 = 0, Zo2 = 1; gl.PointSize(10); gl.Begin(gl.POINTS); // В начале идет красная точка gl.Color(1, 0, 0); gl.Vertex(0, 0, Zo1); // Затем зеленая gl.Color(0, 1, 0); gl.Vertex(0, 0, Zo2); gl.End(); double Zw1, Zw2, Xw1, Xw2, Yw1, Yw2; gl.Project(0, 0, Zo1, mvMtrx, prMtrx, vp, out Xw1, out Yw1, out Zw1); gl.Project(0, 0, Zo2, mvMtrx, prMtrx, vp, out Xw2, out Yw2, out Zw2); stLabel.Text = string.Format("Zw1={0:f}; Zw2={1:f}", Zw1, Zw2); break; В этом примере значение z-координаты у красной точки меньше, чем у зеленой точки. Однако на экране будет видна зеленая точка. Это происходит потому, что тест глубины по умолчанию не активен, а зеленая точка изображается последней. Если тест глубины не активен, то всегда будет изображаться последняя точка, не зависимо от значений координаты глубины. Для включения теста глубины добавьте в начало метода BuildFrame() две строки: gl.Clear(gl.DEPTH_BUFFER_BIT); gl.Enable(gl.DEPTH_TEST); Убедитесь, что видна красная точка, которая находится ближе. Поменяйте значения z-координат точек так, чтобы у зеленой точки z-координата была меньше. Убедитесь, что видна зеленая точка. Теперь, при включенном тесте глубины, точка, имеющая меньшую глубину, будет перекрывать изображение точки с большей глубиной, и это не зависит от последовательности, в которой точки изображаются. В приведенном примере можно так же проверить работу формулы zw = (f – n)/2*zd + (f + n)/2, преобразующей объектную координату глубины zo = zd в оконную координату zw, учитывая, что f = 1 и n = 0 по умолчанию. 44 Методы ClearDepth и DepthFunc Очищается буфер глубины значением, которое указывает метод /// <summary> /// Очищает буфер глубины /// </summary> /// <param name="depth"> /// Значение, которым заполняется буфер глубины. По умолчанию это 1. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glClearDepth")] public static extern void ClearDepth(double depth); Узнать текущее значение, которым очищен буфер глубины, можно методом Get в форме gl.Get(gl.DEPTH_CLEAR_VALUE, 1)[0] Постоянная, использованная здесь, равна /// <summary> /// Аргумент Get требует возврата значения одного параметра /// оконной координаты zw, которым заполняются битовые плоскости /// буфера глубины при его очистке. /// </summary> public const int DEPTH_CLEAR_VALUE = 0x0B73; Тестирование фрагмента, направляемого в буфер глубины, проводится функцией DepthFunc по тому же правилу и с теми же параметрами, что и альфа-тестирование. Метод DepthFunc активен лишь при включенном тестировании глубины. Описание функции DepthFunc имеет вид /// <summary> /// Устанавливает операцию сравнения при работе теста глубины. /// </summary> /// <param name="function"> /// Определяет операцию сравнения глубины. Имеет значения символьных постоянных /// NEVER, LESS, EQUAL, LEQUAL, GREATER, NOTEQUAL, GEQUAL и ALWAYS. /// Значение по умолчанию LESS. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glDepthFunc")] public static extern void DepthFunc(int function); Текущее значение параметра function функции DepthFunc может быть получено методом Get в форме (int)gl.Get(gl.DEPTH_FUNC, 1)[0] Здесь константа DEPTH_FUNC имеет вид /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// целой символьной постоянной, определяющей функцию тестирования глубины. /// </summary> public const int DEPTH_FUNC = 0x0B74; Примечание Казалось бы, если с помощью функции DepthFunc изменить условие тестирования, например, с LESS (установлено по умолчанию) на GREATER, то будет изображена та вершина, у которой значение zw больше. Но по умолчанию буфер глубины очищен максимальным значением zw = 1. Поэтому все фрагменты с меньшими значениями будут отсекаться правилом тестирования. Для эффективного использования функции GREATER следовало бы изменить значение параметра очистки методом ClearDepth. В практическом программировании не рекомендуется изменять условие тестирования по глубине (LESS) и параметр очистки буфера глубины (1), установленные по умолчанию. Посмотрите сцену тестирования глубины в приложении. 45 Пояснения и код для того, кто продолжает строить авторский проект. Примечания 1. 2. В OpenGL есть функция DepthRange, устанавливающая значения параметров n и f. Здесь эта функция использоваться не будет, но, при желании, можно ее описать в классе gl и затем использовать. Так может выглядеть описание /// <summary> /// Определяет отображение нормализованных координат устройства на оконные координаты /// </summary> /// <param name="near"> /// Определяет отображение ближней плоскости на оконные координаты /// </param> /// <param name="far"> /// Определяет отображение дальней плоскости на оконные координаты /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glDepthRange")] public static extern void DepthRange(double near, double far); В OpenGL есть метод DepthMask(bool mask) , который работает по аналогии с методом ColorMask, разрешая и запрещая запись в буфер глубины. По умолчанию параметр mask равен true – запись разрешена. Тест рубежного контроля 41. Каким образом активируется тест глубины? 42. Как очищается буфер глубины? 43. Опишите, как работает тест глубины. 44. Как определить, активирован ли тест глубины? 45. Какой метод задает значение zw, очищающее буфер глубины? 46. Как получить текущее значение zw, очищающее буфер глубины? 47. Какое значение имеет параметр, очищающий буфер глубины, по умолчанию? 48. Какой метод определяет функцию тестирования глубины? 49. Как определить текущие значения функции тестирования глубины? 50. С каким параметром следует вызвать метод PushAttrib, чтобы сохранить в стеке текущие атрибуты тестирования глубины? Тестирование глубины показывает, что по умолчанию, объектная система координат, как и оконная, имеет левую ориентацию (ось x направлена вправо, y – вверх, а z – в глубину экрана), что не очень удобно в приложениях, где обычной является правая система координат. В следующем разделе будет введено важное преобразование от координат наблюдения к координатам отсечения, в котором ось z испытывает отражение. Это позволит использовать правую объектную систему координат. Кроме того, новое преобразование позволит сохранять пропорции объекта (отношение ширины к высоте) при переходе к оконным координатам. 6 Преобразование к координатам отсечения В этом разделе вводится ряд новых методов OpenGL, которые определяют порядок преобразования от одних систем координат к другим. Если в имеющейся версии проекта составить из точек круг, например, единичного радиуса, а затем менять размеры окна, то круг будет деформироваться. Ведь в прямоугольном (не квадратном) окне точки, лежащие с одной стороны круга (например, по горизонтали) и имеющие то же единичное расстояние до центра, окажутся в изображении либо дальше от центра, если окно вытянуто по горизонтали, либо ближе к нему. Круг можно изобразить, используя, например, код gl.Begin(gl.POINTS); 46 for (int i = 0; i < 100; i++) gl.Vertex((float)Math.Cos(2.0 * Math.PI * i / 100), (float)Math.Sin(2.0 * Math.PI * i / 100), 0); gl.End(); Решить проблему можно двумя способами. Параметрам метода Viewport можно придавать всегда такие значения, чтобы вывод проводился только в максимальный квадрат окна. Но в этом случае, оставшаяся часть окна окажется недоступной для изображения. Другой способ состоит в таком преобразовании от координат наблюдения (которые совпадают пока с объектными координатами) к координатам отсечения, которое бы устранило с одной стороны этот дефект, а с другой стороны позволило бы использовать всю площадь окна для вывода изображения. Как уже указывалось выше, преобразования от объектных координат xo, yo, zo , wo к координатам наблюдения xe, ye, ze, we и, далее, к координатам отсечения xc, yc, zc, wc по умолчанию определяются единичными матрицами. Поэтому значения объектных координат xo, yo, zo и "нормализованных координат устройства" xd, yd, zd совпадают, если wo = 1. В общем случае команды OpenGL позволяют вводить произвольные матрицы двух типов. Первый тип преобразования – "от модели к наблюдению", или тип MODELVIEW определяет преобразования между координатами объектной системы и системы координат наблюдения. Второй тип преобразования – от координат наблюдения к координатам отсечения называется проекционным (PROJECTION). Матрицы 4х4 обоих типов преобразований вводятся в отдельные стеки и присутствуют там к моменту построения объекта из примитивов. Эти матрицы уже использовались выше в применении к методу Project. Они были единичными по умолчанию. Но эти матрицы могут быть любыми. Методы MatrixMode Этот метод регулирует режим предполагаемого преобразования. Он используется для указания типа матрицы преобразования, вводимой в стек. /// <summary> /// Устанавливает тип последующих преобразований координат. /// </summary> /// <param name="mode"> /// Определяет тип стека матриц, на содержание которого /// будут влиять последующие преобразования. /// Доступны три значения, /// определяемые символьными постоянными MODELVIEW, PROJECTION и TEXTURE. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glMatrixMode")] public static extern void MatrixMode(int mode); Метод MatrixMode управляется тремя символьными постоянными, указывающими тип преобразования, к которому будут относиться вводимые далее матрицы. Пока что рассмотрим лишь работу с двумя из них /// <summary> /// Последующие операции с матрицами относятся /// к преобразованиям от координат наблюдения /// к координатам отсечения (действуют на стек проекционной матрицы). /// </summary> public const int PROJECTION = 0x1701; /// <summary> /// Последующие операции с матрицами относятся /// к преобразованиям от объектных координат к координатам наблюдения. /// </summary> 47 public const int MODELVIEW = 0x1700; Эти описания следует поместить в класс gl библиотеки GL. Для определения текущего состояния режима MatrixMode можно использовать метод Get в форме (int)gl.Get(gl. MATRIX_MODE, 1)[0] Здесь аргументом является константа /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// целой символьной постоянной, определяющей какой матричный стек в настоящий момент /// может меняться (метод MatrixMode). /// </summary> public const int MATRIX_MODE = 0x0BA0; Методы LoadIdentity и Ortho После вызова метода MatrixMode с соответствующим параметром можно использовать методы, помещающие в указанный стек конкретные матрицы преобразований координат. Метод LoadIdentity вводит в стек единичную матрицу /// <summary> /// Заменяет текущую матрицу в стеке на единичную матрицу. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glLoadIdentity")] public static extern void LoadIdentity(); Метод Ortho умножает матрицу, находящуюся в стеке преобразования от координат наблюдения к координатам отсечения на матрицу ортографической (плоскопараллельной) проекции. Существует две версии этого метода. /// <summary> /// Умножает содержимое стека преобразования /// от координат наблюдения к координатам отсечения /// на матрицу ортографической (параллельной) проекции. /// </summary> /// <param name="left"> /// Положение левой плоскости отсечения. /// Значение x-координаты наблюдения, /// которое проецируется на левую границу порта наблюдения. /// </param> /// <param name="right"> /// Положение правой плоскости отсечения. /// Значение x-координаты наблюдения, /// которое проецируется на правую границу порта наблюдения. /// </param> /// <param name="bottom"> /// Положение нижней плоскости отсечения. /// Значение y-координаты наблюдения, /// которое проецируется на нижнюю границу порта наблюдения. /// </param> /// <param name="top"> /// Положение верхней плоскости отсечения. /// Значение y-координаты наблюдения, 48 /// которое проецируется на верхнюю границу порта наблюдения. /// </param> /// <param name="near"> /// Положение ближней плоскости отсечения. /// </param> /// <param name="far"> /// Положение дальней плоскости отсечения. /// </param> /// <remarks> /// Значения (left, bottom, - near) и (right, top, - near) задают координаты наблюдения точек /// на ближней плоскости отсечения, которые отображаются соответственно /// на левый нижний и правый верхний угол порта наблюдения /// в предположении, что начало координат наблюдения находится в точке (0, 0, 0). /// Значение -far задает положение дальней плоскости отсечения. /// Значения far и near могут быть как положительными, так и отрицательными. /// </remarks> [DllImport("OPENGL32.DLL", EntryPoint = "glOrtho")] public static extern void Ortho(double left, double right, double bottom, double top, double near, double far); /// <summary> /// Умножает содержимое стека преобразования /// от координат наблюдения к координатам отсечения /// на матрицу ортографической (параллельной) проекции. /// Эквивалентен вызову метода Ortho с шестью параметрами, когда два последних параметра /// имеют значения near = -1 и far = 1. /// </summary> [DllImport("GLU32.DLL", EntryPoint = "gluOrtho2D")] public static extern void Ortho(double left, double right, double bottom, double top); Эти описания следует поместить в класс gl библиотеки GL. Матрица преобразования Ortho с обозначениями l – left, r – right, t – top, b – bottom, n – near, f – far имеет вид Преобразование Ortho позволит изменить отображение объектных координат в координаты отсечения таким образом, чтобы прямоугольник порта наблюдения совпадал по пропорциям с прямоугольником в объектных координатах. Это уберет искажения. Применить описанные методы в коде проекта можно следующим образом. 0. Ввести в класс формы f3D // поля, хранящие область отсечения float prjLeft, prjRight, prjBottom, prjTop; // поля, хранящие ширину и высоту порта наблюдения. float vpWidth, vpHeight; 1. Ввести в класс f3D метод InitProjection со следующим содержанием 49 /// <summary> /// Устанавливает преобразование от координат наблюдения к координатам отсечения /// </summary> void InitProjection() { // Определение текущей ширины и высоты порта наблюдения vpWidth = gl.Get(gl.VIEWPORT, 4)[2]; vpHeight = gl.Get(gl.VIEWPORT, 4)[3]; if (vpWidth > vpHeight) { prjLeft = -(prjRight = vpWidth / vpHeight); prjBottom = -(prjTop = 1); } else { prjLeft = -(prjRight = 1); prjBottom = -(prjTop = vpHeight / vpWidth); } // Устанавливается режим ввода матрицы в стек преобразования // от координат наблюдения к координатам отсечения gl.MatrixMode(gl.PROJECTION); // Ввод единичной матрицы gl.LoadIdentity(); // Ввод матрицы ортографической проекции с учетом пропорций порта наблюдения gl.Ortho(prjLeft, prjRight, prjBottom, prjTop); } 2. Поместить вызов метода InitProjection() в обработчик panelGL_SizeChanged сразу после вызова метода gl.Viewport. Теперь пропорции будут соблюдены, и окружность сохраняет свою форму при изменении пропорций окна. Убедитесь в этом, вызвав иллюстрирующее приложение. Можете провести изменения в коде своего приложения, либо посмотреть, как на этом этапе редактируется авторское приложение. Кроме сохранения пропорций, матрица ортографического преобразования, примененная здесь, отражает ось глубины, что делает систему объектных координат правой. Это следует из вида матрицы преобразования Ortho, приведенной выше. Если подставить в нее значения n = -1, f = 1, что соответствует вызову методе Ortho с 4-мя параметрами, то компонента zz матрицы ортографической проекции -2/(f – n) будет равна -1. А это соответствует отражению по оси z. В этом можно также убедиться, используя пример кода, приведенный в разделе теста глубины. Тест рубежного контроля 51. Что делает метод MatrixMode? 52. Что представляет собой преобразование, задаваемое методом Ortho? 53. Как ввести на стек преобразований координат единичную матрицу? В следующем разделе будут рассмотрены методы, преобразующие систему объектных координат к координатам наблюдения. Эти методы позволяют поворачивать, сдвигать и менять видимые размеры объекта на экране, а так же менять ракурс изображения, рассматривая объект с разных сторон. 7 Методы преобразования от объектных координат к координатам наблюдения Часто бывает необходимо изменить положение, ориентацию и размеры объекта на экране, не меняя его геометрии. Для этого следует использовать преобразования от объектных координат, жестко связанных с 50 объектом, к координатам наблюдения, жестко связанных с экраном. В этом разделе рассматриваются стандартные методы сдвига, вращения и масштабного преобразования объектной системы координат при переходе к системе наблюдения. Координаты наблюдения по умолчанию совпадают с объектными координатами. Однако можно проводить преобразования, которые поворачивают, смещают объект или меняют его видимые размеры и пропорции. В этом случае объектная система координат поворачивается, смещается или меняются масштабы вдоль ее осей по сравнению с ее первоначальным положением. Неподвижное исходное положение объектной системы координат и представляет собой систему координат наблюдения. Методы Translate, Rotate Так выглядят заголовки этих двух методов /// <summary> /// Умножает текущую матрицу на матрицу, транслирующую /// начало системы координат на вектор x, y, z. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glTranslatef")] public static extern void Translate(float x, float y, float z); /// <summary> /// Умножает текущую матрицу на матрицу поворота на угол angle, измеряемый в градусах, /// относительно оси, заданной вектором x, y, z в объектной системе координат /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glRotatef")] public static extern void Rotate(float angle, float x, float y, float z); Эти описания следует поместить в класс gl библиотеки GL. Матрица преобразования, отвечающая трансляции, имеет вид . Матрица преобразования, отвечающая вращению на угол θ относительно оси с единичным вектором u(x', y', z'), имеет вид Здесь матрица поворота R определяется формулой R = uuT + cosθ (I - uuT) + sinθ S, T Где символ означает транспонирование, I – единичная матрица, а матрица S имеет вид Методы Translate, Rotate и последующие методы, описанные в этом разделе, будут использоваться для преобразований объектной системы координат к координатам наблюдения. Поэтому они вводятся в соответствующий стек преобразований. Установить режим ввода в стек преобразований от объектных координат к координатам наблюдения можно в конце метода InitProjection в форме кода // Устанавливается режим ввода матрицы в стек преобразования // от объектных координат к координатам наблюдения 51 gl.MatrixMode(gl.MODELVIEW); // На вершину стека помещается единичная матрица преобразований // от объектных координат к координатам наблюдения gl.LoadIdentity(); Методы Translate и Rotate можно использовать в сочетании с таймером. Для использования таймера из раздела Components окна Toolbox перенесите на форму f3D объект класса Timer. Дайте ему имя (свойство Name в окне Properties) timer. Выберите обработчик события Tick таймера в этом же окне Properties странице обработчиков (кнопка с изображением молнии). Добавьте к полям формы f3D описание 4-ех новых полей, регулирующих параметры вращения и трансляции изображения /// <summary> /// Хранит текущий угол поворота объектной системы координат вокруг своей оси. /// </summary> float angleRotate; /// <summary> /// Хранит текущий угол поворота объектной системы координат вокруг внешней оси. /// </summary> float angleTranslate; // поля, хранящие состояние активности вращения и трансляции bool rotateEnabled, translateEnabled; На каждом тике таймера меняется параметр сдвига и/или поворота в зависимости от флагов, и изображается новый кадр, где объект уже оказывается сдвинутым и/или повернутым. Например, код в обработчике события Tick таймера может выглядеть следующим образом if (translateEnabled) angleTranslate++; if (rotateEnabled) angleRotate++; RenderFrame(); Здесь параметры сдвига и поворота содержатся в полях angleTranslate, angleRotate, а флаги их активности – в полях translateEnabled и rotateEnabled соответственно (см. также ниже). Метод Scale Метод Scale меняет масштаб изображения и его удобно использовать в сочетании с вращением колесика мышки. Поворот колесика меняет масштабный коэффициент и создается впечатление, что наблюдатель удаляется или приближается к объекту. /// <summary> /// Умножает текущую матрицу на матрицу масштабного преобразования. /// Параметрами x, y, z задаются масштабные множители вдоль соответствующих осей. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glScalef")] public static extern void Scale(float x, float y, float z); Поместите это описание в класс gl библиотеки GL. Матрица преобразования масштабов имеет вид Для примера использования метода Scale в класс f3D добавьте поля 52 /// <summary> /// Определяет масштаб вращения колесика мышки /// </summary> const float wheelScale = 0.003f; /// <summary> /// Хранит текущий масштабный коэффициент изображения объекта. /// </summary> float scaleCoeff; в класс f3D добавьте обработчик вращения колесика мышки MouseWheel над панелью panelGL. Этот обработчик отсутствует в окне Properties, поэтому его следует добавить непосредственно в конструкторе формы f3D. С этой целью в конце тела этого конструктора (метод public f3D) начните набирать строку кода panelGL.MouseWheel += На этом этапе должна возникнуть подсказка, предлагающая нажать клавишу Tab. Нажмите один раз и затем второй. Это приведет к тому, что a. строка кода автоматически завершится и примет вид panelGL.MouseWheel += new MouseEventHandler(panelGL_MouseWheel); b. появится скелета обработчика panelGL_MouseWheel в форме void panelGL_MouseWheel(object sender, MouseEventArgs e) { throw new NotImplementedException(); } Уберите строку throw… внутри обработчика и поставьте вместо нее операторы, меняющие масштабный коэффициент в заисимости от поворота колесика scaleCoeff *= (float)(e.Delta > 0 ? 1.0 / (1.0 + wheelScale * e.Delta) : 1.0 + wheelScale * Math.Abs(e.Delta)); RenderFrame(); Здесь поле scaleCoeff содержит текущий масштаб изображения (аргумент метода Scale, см. также ниже), а поле wheelScale хранит некоторое постоянное значение, характеризующие плавность изменения масштаба. В данном случае значение wheelScale принято равным 0,003. Для обработки события от колесика необходимо, чтобы панель panelGL оказывалась в фокусе при входе в нее курсора мышки (если панель не в фокусе, она не будет воспринимать информацию о повороте колесика мышки). Это обеспечивается обработкой события MouseEnter. Поэтому, найдите в окне Properties панели panelGL событие MouseEnter, и в его обработчик поместите строку кода, устанавливающую фокус на панель panelGL при попадании туда курсора мышки (sender as Control).Focus(); Метод LookAt Метод LookAt вносит в стек такую матрицу, которая меняет положение наблюдателя по отношению к объекту /// <summary> /// Определяет преобразование наблюдения, устанавливая точку положения наблюдателя /// и видимое направление вертикали. /// Вектор eyex, eyey, eyez определяет положение наблюдателя. /// Вектор centerx, centery, centerz определяет положение центра сцены. /// Вектор upx, upy, upz - видимое направление вертикали. /// </summary> 53 [DllImport("GLU32.DLL", EntryPoint = "gluLookAt")] public static extern void LookAt(double eyex, double eyey, double eyez, double centerx, double centery, double centerz, double upx, double upy, double upz); Описания LookAt следует поместить в класс gl библиотеки GL. Метод LookAt удобно использовать совместно с обработчиками событий MouseDown и MouseMove мышки на панели panelGL с тем, чтобы создать эффект регулируемого вращения наблюдателя относительно изображенного объекта. Добавьте в класс f3D формы следующие поля /// <summary> /// Хранит текущую ориентацию камеры вида по отношению к вертикальной оси. /// Угол cameraFi отсчитывается от вертикальной средней линии вправо /// до Pi на правой границе панели и влево до -Pi на левой границе. /// </summary> double cameraFi; double cameraSinFi, cameraCosFi; /// <summary> /// Хранит текущую ориентацию камеры вида по отношению к горизонтальной оси. /// Угол cameraTeta отсчитывается от горизонтальной плоскости. /// Вверх - положительный, вниз - отрицательный. Это широта. /// cameraTeta лежит в интервале -Pi/2;Pi/2. /// </summary> double cameraTeta; double cameraSinTeta, cameraCosTeta; /// <summary> /// Хранит положение курсора в момент нажатия левой кнопки над glPanel /// </summary> Point curCursorPos; Найдите обработчики событий MouseDown и MouseMove мышки на панели panelGL (окно Properties) и внесите в них следующий код В обработчике MouseDown if (e.Button == MouseButtons.Left) curCursorPos = e.Location; В обработчике MouseMove if (e.Button == MouseButtons.Left && panelGL.ClientRectangle.Contains(e.Location)) { cameraFi += 2.0 * (e.X - curCursorPos.X) / panelGL.ClientSize.Width * Math.PI; cameraTeta += (-1.0 * (e.Y - curCursorPos.Y) / panelGL.ClientSize.Height) * Math.PI; // Запоминается новое положение курсора curCursorPos = e.Location; // Вычисляются тригонометрические функции, используемые // в LookAt для расчета матрицы преобразования cameraCosTeta = Math.Cos(cameraTeta); cameraSinTeta = Math.Sin(cameraTeta); cameraSinFi = Math.Sin(cameraFi); cameraCosFi = Math.Cos(cameraFi); RenderFrame(); } 54 Здесь поле curCursorPos формы f3D хранит текущее положение курсора мышки на панели panelGL. Его значение обновляется при нажатии левой клавиши мышки и при движении мышки по панели. Поля cameraCosTeta, cameraSinTeta, cameraSinFi, cameraCosFi хранят текущую информацию о положении наблюдателя по отношению к объекту. Эта информация передается методу LookAt (см. также ниже). Все перечисленные методы преобразования к координатам наблюдения Translate, Rotate, Scale и LookAt, следует вызывать перед каждым фреймом. Опишите новые поля в классе f3D формы /// <summary> /// Радиус сферы наблюдения /// </summary> const float radiusEye = .001f; // Компоненты единичного вектора оси вращения. float rX, rY, rZ; /// <summary> /// Хранит текущий радиус окружности, в точки которой транслируется начало объектной стсьемы координат. /// </summary> float radiusTranslate; /// <summary> /// Хранит объект, возвращающий случайные числа. /// </summary> Random rnd = new Random(); Добавьте к классу метод, вызывающий все четыре метода преобразования координат (отметим, что операции трансляции и вращения не коммутируют, то есть их результат зависит от последовательности, в которой операции совершались) /// <summary> /// Устанавливает текущую матрицу преобразования от объектных координат /// к координатам наблюдения /// </summary> void SetCurModelViewTransformation() { gl.LoadIdentity(); gl.Translate(radiusTranslate * (float)Math.Cos(angleTranslate * Math.PI / 180), radiusTranslate * (float)Math.Sin(angleTranslate * Math.PI / 180), 0); gl.Rotate(angleRotate, rX, rY, rZ); gl.Scale(scaleCoeff, scaleCoeff, scaleCoeff); // Установка "камеры наблюдения" - ввод специальной матрицы gl.LookAt( //Текущие координаты наблюдателя на сфере с центром в начале координат radiusEye * cameraCosTeta * cameraSinFi, radiusEye * cameraSinTeta, radiusEye * cameraCosTeta * cameraCosFi, //Неподвижная точка наблюдения 0, 0, 0, //Направление вверх, нормальное к линии, соединяющей наблюдателя и // неподвижную точку (центр сферы) -cameraSinTeta * cameraSinFi, cameraCosTeta, -cameraSinTeta * cameraCosFi); 55 } Приведенный метод SetCurModelViewTransformation следует вызывать в начале метода BuildFrame. В этом методе присутствуют поля, хранящие текущие значения различных параметров. Некоторые из них, такие как радиус окружности radiusTranslate, на который транслируется изображение методом Translate, единичный вектор оси поворота rX, rY, rZ в методе Rotate и радиус сферы наблюдения radiusEye в методе LookAt могут быть постоянными от фрейма к фрейму. В то же время, угол поворота по окружности angleTranslate в методе Translate, угол вращения angleRotate в методе Rotate, масштабный коэффициент scaleCoeff в методе Scale и тригонометрические функции cameraCosTeta, cameraSinFi, cameraSinTeta, cameraCosFi положения наблюдателя на сфере в методе LookAt могут меняться от фрейма к фрейму, если включен таймер (для Rotate и/или Translate), либо крутится колесико (для Scale), либо, наконец, меняется положение курсора мышки с нажатой левой кнопкой (для LookAt). Эти изменения даются в приведенных выше примерах. Для инициализации рассмотренных параметров в начало метода InitSceneAttributes следует поместить вызов соответствующего метода void InitModelViewTransfParameters() { rotateEnabled = translateEnabled = timer.Enabled = false; // Случайные углы направления оси вращения объектной системы координат. double fi = rnd.NextDouble() * 2 * Math.PI, teta = rnd.NextDouble() * Math.PI; // Единичный вектор оси вращения объектной системы координат. rX = (float)(Math.Sin(teta) * Math.Cos(fi)); rY = (float)(Math.Sin(teta) * Math.Sin(fi)); rZ = (float)Math.Cos(teta); scaleCoeff = 1; // Начальное положение камеры cameraTeta = .0; cameraFi = .0; cameraCosTeta = Math.Cos(cameraTeta); cameraSinTeta = Math.Sin(cameraTeta); cameraSinFi = Math.Sin(cameraFi); cameraCosFi = Math.Cos(cameraFi); angleRotate = angleTranslate = radiusTranslate = 0; } Включать и выключать процессы трансляции и поворота можно отдельными кнопками, по клику которых должны меняться значения флагов rotateEnabled, translateEnabled и включаться/ выключаться таймер. Так же кнопкой можно приводить изображение в начальный вид, вызывая метод InitModelViewTransfParameters. Кнопки можно добавить к левой или правой панели объекта toolStripContainer1, находящегося на форму f3D. Например, для активации вращения можно на правую панель перетащить компоненту класса ToolStrip из раздела Menus & Toolbars окна Toolbox; Щелкнув по стрелочке добавленного объекта, из выпадающего списка можно выбрать кнопку (Button); В окне Properties этой кнопки изменить значения некоторых ее свойств: o Дать ей имя (Name) btnRotate; o Свойство DisplayStyle - выбрать Text o Свойство Image – выбрать None (стереть картинку, данную по умолчанию) o Свойство Text – Rotate o Свойство TextDirection – Vertical90 56 o Выбрать обработчик события Click этой кнопки и вписать в него код btnRotate.Text = (rotateEnabled = !rotateEnabled) ? "Stop" : "Rotate"; if (rotateEnabled && !timer.Enabled) timer.Enabled = true; if (!rotateEnabled && !translateEnabled) timer.Enabled = false; Аналогичные действия следует провести для новой кнопки, которая будет активировать процесс трансляции изображения, назвав кнопку btnTranslate и поместив в обработчик ее клика код btnTranslate.Text = (translateEnabled = !translateEnabled) ? "Stop" : "Translate"; radiusTranslate = translateEnabled ? .5f : 0; RenderFrame(); if (translateEnabled && !timer.Enabled) timer.Enabled = true; if (!rotateEnabled && !translateEnabled) timer.Enabled = false; Разместите самостоятельно на левой панели новую кнопку, назвав ее btnRestore. Напишите код обработчика клика кнопки btnRestore, возвращающей изображение в исходное состояние. Применение метода LookAt приведет к одному дефекту изображения. При некоторых положениях наблюдателя часть точек объекта, заключенного внутри куба (-1;1), который используется здесь в соответствии с параметрами метода Ortho, будет отсекаться. Эффект объясняется тем, что эти точки при некотором ракурсе, возникающем в преобразовании LookAt, оказываются на расстоянии по оси z большим единицы. Срабатывает эффект отсечения, установленный матрицей Ortho, где по умолчанию дальняя отсекающая плоскость по z находится на расстоянии 1 от наблюдателя. Но расстояние до самой дальней точки куба с ребром 2 от его центра равно половине длины его диагонали, то есть 31/2. Поэтому следует выбрать дальнюю плоскость отсечения на расстоянии, по крайней мере, равном 31/2 + радиус сферы наблюдения (radiusEye). Это расстояние до самой дальней точки куба, на котором может находиться наблюдатель при применении метода LookAt. Так может выглядеть новая редакция метода InitProjection, устраняющая наблюдаемый дефект void InitProjection() { // Определение текущей ширины и высоты порта наблюдения vpWidth = gl.Get(gl.VIEWPORT, 4)[2]; vpHeight = gl.Get(gl.VIEWPORT, 4)[3]; if (vpWidth > vpHeight) { prjLeft = -(prjRight = vpWidth / vpHeight); prjBottom = -(prjTop = 1); } else { prjLeft = -(prjRight = 1); prjBottom = -(prjTop = vpHeight / vpWidth); } prjFar = -(prjNear = (float)Math.Sqrt(3) + radiusEye); // Устанавливается режим ввода матрицы в стек преобразования // от координат наблюдения к координатам отсечения gl.MatrixMode(gl.PROJECTION); // Ввод единичной матрицы gl.LoadIdentity(); // Умножает текущую матрицу на матрицу ортографической проекции 57 // с учетом пропорций порта наблюдения gl.Ortho(prjLeft, prjRight, prjBottom, prjTop, -prjNear, -prjFar); // Устанавливается режим ввода матрицы в стек преобразования // от объектных координат к координатам наблюдения gl.MatrixMode(gl.MODELVIEW); gl.LoadIdentity(); } Посмотрите результат редакций этого раздела. Для того, кто строит приложение, ссылка на код. Кроме упомянутых методов преобразования координат OpenGL содержит и другие методы 1. LoadMatrix загружает на вершину стека произвольную матрицу преобразования 2. MultMatrix умножает матрицу, находящуюся в вершине стека, на произвольную матрицу В дальнейшем в этом же руководстве будут рассмотрены методы Frustum и Perspective, которые обеспечивают изображение объектов в перспективной проекции. В отличие от ортографической проекции перспективная проекция создает эффект уменьшения удаляющегося объекта. Тест рубежного контроля 54. Что делает метод Translate? 55. Какой метод совершает преобразование поворота? 56. Какой метод производит масштабное преобразование? 57. Что делает метод LookAt? 8 Дисплейные списки. Методы IsList, GenLists, DeleteLists, NewList, EndList, CallList Построение любого объекта лучше оформлять в виде списка команд, который остается неизменным в памяти компьютера и может быть многократно воспроизведен ссылкой на его номер. Для формирования списков команд используются ряд методов, описанных в библиотеке OpenGL. Вот описание констант и методов, которые участвуют в работе с дисплейными списками команд /// <summary> /// Команды создаваемого списка компилируются и выполняются /// </summary> public const int COMPILE_AND_EXECUTE = 0x1301; /// <summary> /// Команды списка только компилируются при его создании /// </summary> public const int COMPILE = 0x1300; /// <summary> /// Проверяет наличие списка /// </summary> /// <param name="list"> /// Имя искомого списка /// </param> /// <returns> /// true, если список существует. В противном случае false. /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "glIsList")] 58 public static extern bool IsList(uint list); /// <summary> /// Убирает списки из памяти, освобождая их имена. /// </summary> /// <param name="list"> /// Номер первого из уничтожаемых списков /// </param> /// <param name="range"> /// Число уничтожаемых списков /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glDeleteLists")] public static extern void DeleteLists(uint list, int range); /// <summary> /// Создает непрерывный ряд пустых списков /// </summary> /// <param name="range"> /// Число создаваемых списков /// </param> /// <returns> /// Номер (имя) первого из созданных списков /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "glGenLists")] public static extern uint GenLists(int range); /// <summary> /// Создает новый список с данным именем или заменяет существующий /// </summary> /// <param name="list"> /// Имя (номер) списка. /// </param> /// <param name="mode"> /// Режим компиляции команд списка при его создании. /// Может иметь значения COMPILE или COMPILE_AND_EXECUTE. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glNewList")] public static extern void NewList(uint list, int mode); /// <summary> /// Завершает создание списка команд. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glEndList")] public static extern void EndList(); /// <summary> /// Исполняет команды списка. /// </summary> /// <param name="list"> /// Имя (номер) списка команд. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glCallList")] public static extern void CallList(uint list); 59 Текущие значения аргументов метода NewList в процессе создания списка (до обращения к EndList) можно получить с помощью метода Get с аргументами /// <summary> /// Аргумент Get требует возврата значения 1 параметра - режима, /// в котором создается текущий список (аргумент NewList). /// </summary> public const int LIST_MODE = 0x0B30; /// <summary> /// Аргумент Get требует возврата значения 1 параметра - текущего номера /// создаваемого списка (аргумент NewList). /// </summary> public const int LIST_INDEX = 0x0B33; Эти описания следует поместить в класс gl библиотеки GL. Использование методов работы с дисплейными списками можно проиллюстрировать на примере списка команд, создающих окружность из отдельных точек. Вот пример метода, создающего и выполняющего список точек окружности /// <summary> /// Создает список окружности из точек и возвращает его номер /// </summary> /// <param name="pointsNmb"> /// Число точек на окружности /// </param> /// <param name="pointsCircleList"> /// Номер списка, возвращаемого методом /// </param> void MakePointsCircleList(int pointsNmb, ref uint pointsCircleList) { // Если номер списка не равен нулю и список существует, // то список стирается и номер зануляется if (pointsCircleList != 0 && gl.IsList(pointsCircleList)) { gl.DeleteLists(pointsCircleList, 1); pointsCircleList = 0; } // Номер списка выбирается из свободных номеров pointsCircleList = gl.GenLists(1); /* // Работает так же более простой вариант кода // Если список не создан (номер равен нулю) if (0 == pointsCircleList) // Номер списка выбирается из свободных номеров pointsCircleList = gl.GenLists(1); // Следующий далее метод NewList будет выполняться и в том случае, // когда номер pointsCircleList списка занят. // Прежнему номеру будет соотнесен новый список, а прежний список уже не будет доступен. */ // Начало создания нового списка. // Список создается и тут же воспроизводится gl.NewList(pointsCircleList, gl.COMPILE_AND_EXECUTE); 60 // Команды списка - окружность из точек gl.Begin(gl.POINTS); for (int i = 0; i < pointsNmb; i++) gl.Vertex((float)Math.Cos(2.0 * Math.PI * i / pointsNmb), (float)Math.Sin(2.0 * Math.PI * i / pointsNmb), 0); gl.End(); // Завершаются команды списка gl.EndList(); } Метод MakePointsCircleList может вызываться внутри метода BuildFrame в том случае, если число точек окружности меняется, либо если список еще не готов. Если это число не меняется (например, окружность поворачивается, или воспроизводится в каком-то другом ракурсе), то вызывается просто сам готовый список методом CallList. Это может выглядеть следующим образом if (curCirclePointsNmb != CirclePointsNmb || 0 == curPointsCircleList) MakePointsCircleList(CirclePointsNmb = curCirclePointsNmb, ref curPointsCircleList); else gl.CallList(curPointsCircleList); Здесь поле curCirclePointsNmb содержит текущее значение числа точек окружности, а CirclePointsNmb – предыдущее значение этого же числа. Если эти два числа не совпадают, либо если текущий номер списка curPointsCircleList равен нулю, то вызывается метод MakePointsCircleList. При этом прежнее значение числа точек заменяется новым значением. В противном случае список curPointsCircleList просто воссоздается в прежнем виде вызовом метода CallList. Поле curPointsCircleList следует обнулять, а поле CirclePointsNmb инициализировать каким-либо значением (например, 100) в методе InitSceneAttributes. Значение поля curCirclePointsNmb должно быть переменным в результате действий пользователя, как, например, в иллюстрирующем приложении. Ту же идею воссоздания списка и его воспроизведения можно реализовать несколько иначе. Для этого в коде класса f3D Используйте отдельные объекты-обработчики класса delegate со следующим описанием класса /// <summary> /// Определяет тип методов, создающих дисплейные списки объектов /// </summary> delegate void CreateList(); Опишите метод, использующий в качестве одного из своих параметров ссылку на объект этого класса, то есть на метод создания списка конкретного объекта /// <summary> /// Создает список дисплейных команд, одновременно вызывая его, либо вызывает уже готовый список /// </summary> /// <param name="isListCreated"> /// Создан ли список /// </param> /// <param name="listNmb"> /// Номер списка /// </param> /// <param name="createList"> 61 /// Метод создания списка класса CreateList /// </param> void CreateNCallList(bool isListCreated, uint listNmb, CreateList createList) { if (!isListCreated) createList(); else gl.CallList(listNmb); } Используя этот подход, приведенный пример организации списка точек, образующих окружность, можно сформулировать следующим образом Опишите поля для хранения флага создания списка окружности, номера списка, текущего числа точек в окружности /// <summary> /// Хранит true, если список с нужным числом точек создан /// </summary> bool _isCircleListCreated; /// <summary> /// Хранит текущий номер дисплейного списка окружности из точек. /// </summary> uint _circleList; /// <summary> /// Хранит текущее число точек в окружности. /// </summary> int _pointsNmb = 100; Опишите свойство числа точек, которое обеспечивает автоматический сброс флага создания списка окружности при изменении числа точек /// <summary> /// Устанавливает и возвращает число точек в списке окружности. /// При установке сбрасывает флаг создания создания списка. /// </summary> int pointsNmb { set { _pointsNmb = value; _isCircleListCreated = false; } get { return _pointsNmb; } } Опишите метод создания списка окружности, который должен быть третьим параметром при вызове метода CreateNCallList /// <summary> /// Создает дисплейный список окружности, состоящей из точек /// </summary> void CreateCircleList() { // Если список существует, то список стирается if (gl.IsList(_circleList)) gl.DeleteLists(_circleList, 1); // Номер списка выбирается из свободных номеров _circleList = gl.GenLists(1); 62 // Начало создания нового списка. // Список создается и тут же воспроизводится gl.NewList(_circleList, gl.COMPILE_AND_EXECUTE); // Команды списка - окружность из точек // В стек заталкиваются атрибуты точек и буфера цвета gl.PushAttrib(gl.POINT_BIT | gl.CURRENT_BIT); // Устанавливаются свои атрибуты точек и их цвет gl.PointSize(5); gl.Enable(gl.POINT_SMOOTH); gl.Color(0, 1, 1); // Изображаются точки на окружности, число которых определяется свойством pointsNmb gl.Begin(gl.POINTS); for (int i = 0; i < pointsNmb; i++) gl.Vertex((float)Math.Cos(2.0 * Math.PI * i / pointsNmb), (float)Math.Sin(2.0 * Math.PI * i / pointsNmb), 0); // Из стека удаляются временно установленные атрибуты точек и их цвет gl.PopAttrib(); gl.End(); // Завершаются команды списка gl.EndList(); // Поднимается флаг создания списка – создан новый список окружности _isCircleListCreated = true; } После этих описаний в точке воспроизведения списка (метод BuildFrame) можно просто вызвать метод CreateNCallList(_isCircleListCreated, _circleList, CreateCircleList); В другой, более консервативной части кода (например, в обработчике какого-либо события, при котором необходимо изменить число точек на окружности) можно менять значение свойства pointsNmb. При этом флаг создания списка _isCircleListCreated будет сбрасываться и следующий вызов метода CreateNCallList приведет к созданию и воспроизведению нового списка, с новым числом точек. Преимущество приведенного алгоритма в том, что для создания и вызовов других списков не надо менять метод CreateNCallList, а лишь его параметры. Для того, кто создает авторское приложение, ссылка на код. В следующих разделах будут использованы другие методы работы со списками на примере списков команд, изображающих символы. Тест рубежного контроля 58. Зачем нужны дисплейные списки команд? 59. Какие два метода создают дисплейный список команд? 60. Какой смысл параметра COMPILE метода NewList? 61. Какой смысл параметра COMPILE_AND_EXECUTE метода NewList? 62. Что делает метод GenLists? 63. В чем смысл метода IsList? 64. Что делает метод DeleteLists? 65. Поясните смысл метода CallList. 63 9 Вывод символов. GDI-функции Существует, по крайней мере, три способа подготовки и вывода символов и текста на панель воспроизведения команд OpenGL. Первый способ состоит в использовании стандартных функций библиотеки GDI32. Текст выводится на панель panelGL так же, как если бы это было обычное окно. Поместите в класс gl библиотеки GL описание трех методов и одной константы /// <summary> /// Устанавливает цвет текста Color для указанного контекста устройства deviceContext /// </summary> /// <returns> /// Установленный цвет при успешном завершении /// </returns> [DllImport("GDI32")] public static extern int SetTextColor(int deviceContext,int Color); /// <summary> /// Устанавливает фоновый цвет Color для устройства deviceContext /// </summary> /// <returns> /// Установленный фоновый цвет при успешном завершении /// </returns> [DllImport("GDI32")] public static extern int SetBkColor(int deviceContext, int Color); /// <summary> /// Изображает строку символов в устройстве deviceContext, используя текущий шрифт /// </summary> /// <returns> /// true при успешном завершении /// </returns> [DllImport("GDI32")] public static extern bool ExtTextOut(int deviceContext, int x, int y, uint options, IntPtr rect, string text, uint count, IntPtr spacing); /// <summary> /// Используется как аргумент метода Get, который в этом случае требует возврата значений /// четырех параметров цвета заполнения буфера цвета. /// </summary> public const int COLOR_CLEAR_VALUE = 0x0C22; В класс f3D поместите описание структуры, содержащей данные о выводимом тексте struct GDITextAttr { internal string gdiText; internal Color gdiColor; internal int X, Y; } Добавьте к форме f3D описание метода /// <summary> /// Выводит текст на панель glPanel. /// </summary> /// <param name="text"> 64 /// Строка текста. /// </param> void setGDIText(GDITextAttr gdiAttr) { // Установка цвета текста gl.SetTextColor(port.deviceContext, gdiAttr.gdiColor.R + (gdiAttr.gdiColor.G << 8) + (gdiAttr.gdiColor.B << 16)); // Определение цвета наполнения буфера OpenGL - цвета фона для текста int bkColor = 0; for (int i = 0; i < 3; i++) bkColor += (int)(gl.Get(gl.COLOR_CLEAR_VALUE, 4)[i] * 255) << i * 8; // Установка цвета фона для текста gl.SetBkColor(port.deviceContext, bkColor); // Вывод строки текста в указанную точку панели gl.ExtTextOut(port.deviceContext, gdiAttr.X, gdiAttr.Y, 0, IntPtr.Zero, gdiAttr.gdiText, (uint)gdiAttr.gdiText.Length, IntPtr.Zero); } В конец метода RenderFrame формы f3D (после вызова метода SwapBuffers) добавьте вызов метода setGDIText с конкретным параметром. Параметр имеет тип структуры GDITextAttr, и конкретноые значения ее полей можно поставить в зависимость от данных, регулируемых пользователем извне, либо менять их редактированием в коде программы. Для того, кто создает авторское приложение, ссылка на код. Иллюстрацию к разделу можно посмотреть по ссылке. Тест рубежного контроля 66. Что делает метод SetTextColor? 67. Зачем использовать метод SetBkColor? 68. Что делает метод ExtTextOut? 69. Как определяется положение GDI-текста в окне? 10 BMP-символы Второй способ вывода текста использует команды OpenGL для создания битовых карт символов и произвольного размещения символов в объектной системе координат. Для подготовки и использования списков символов в форме битовых карт используются дополнительные методы по работе с дисплейными списками. Методы ListBase и CallLists Метод ListBase устанавливает номер списка, который будет добавляться ко всем последующим номерам списков, вызываемых с помощью CallList. Это так называемый базовый номер. /// <summary> /// Устанавливает целое смещение, которое после суммирования с относительными /// номерами списков метода CallLists определяет абсолютные номера этих списков. /// По умолчанию смещение равно 0. /// </summary> /// <param name="basevalue"> /// Значение смещения. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glListBase")] 65 public static extern void ListBase(uint basevalue); Текущее значение базового номера списка можно получить с помощью метода Get с аргументом /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущего базового номера списков (аргумент метода ListBase). /// </summary> public const int LIST_BASE = 0x0B32; Для сохранения в стеке текущего номера базового списка следует вызвать метод PushAttrib с аргументом /// <summary> /// Маска битов базового номера списков. /// </summary> public const int LIST_BIT = 0x00020000; Метод CallLists позволяет вызывать сразу несколько списков, что необходимо при формировании текста из нескольких символов. /// <summary> /// Вызывает списки команд OpenGL, начиная с базового номера, установленного ListBase. /// </summary> /// <param name="n"> /// Число вызываемых списков. /// </param> /// <param name="type"> /// Тип номера списка. Допустимы символьные константы BYTE, UNSIGNED_BYTE, /// SHORT, UNSIGNED_SHORT, INT, UNSIGNED_INT, FLOAT, _2_BYTES, _3_BYTES и _4_BYTES. /// </param> /// <param name="lists"> /// Строка из n символов списков. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glCallLists")] public static extern void CallLists(int n, int type, string lists); В данном случае будут использоваться списки, номера которых имеют тип беззнаковых байтов, поэтому понадобится только аргумент в виде символьной константы /// <summary> /// Тип номеров списков - один из аргументов CallLists /// </summary> public const int UNSIGNED_BYTE = 0x1401; Эти описания следует поместить в класс gl библиотеки GL. Методы RasterPos и UseFontBitmap Для вывода битовой карты в буфер фрейма необходимо указать координаты ее верхнего левого угла. Это делает метод RasterPos /// <summary> /// Устанавливает объектные координаты точки вывода изображения. /// Используется при размещении битовых карт, символов. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glRasterPos3f")] public static extern void RasterPosf(float x, float y, float z); 66 Для определения текущих значений параметров метода RasterPos следует использовать метод Get с параметрами /// <summary> /// Аргумент Get требует возврата значений 4 параметров цвета растра(метод RasterPos). /// </summary> public const int CURRENT_RASTER_COLOR = 0x0B04; /// <summary> /// Аргумент Get требует возврата 4 параметров положения растра (метод RasterPos). /// </summary> public const int CURRENT_RASTER_POSITION = 0x0B07; /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// расстояния от начала координат наблюдения до положения растра (метод RasterPos). /// </summary> public const int CURRENT_RASTER_DISTANCE = 0x0B09; Для сохранения текущих значений параметров метода RasterPos следует вызвать метод PushAttrib с параметром CURRENT_BIT. Для выбора шрифта символов используется специальная функция SelectObject из библиотеки GDI32 /// <summary> /// Сопоставляет объект objectHandle контексту устройства deviceContext. /// Новый объект заменяет предыдущий объект того же типа. /// </summary> /// <returns> /// Идентификатор (хэндл) замещенного объекта /// </returns> [DllImport("GDI32")] public static extern IntPtr SelectObject(int deviceContext, IntPtr objectHandle); Главным методом, позволяющим формировать битовые карты из символов заданного шрифта, является метод UseFontBitmaps /// <summary> /// Создает набор списков битовых карт символов текущего шрифта, /// связанного с контекстом устройства deviceContext. /// Списки нумеруются, начиная с номера listBase. /// Номера символов шрифта нумеруются, начиная с номера start. /// Число символов равно count. /// Эти битовые карты могут использоваться для отображения символов в окне OpenGL. /// </summary> /// <returns> /// true при успешном завершении /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "wglUseFontBitmaps")] public static extern bool UseFontBitmaps(int deviceContext, int start, int count, uint listBase); Эти описания следует поместить в класс gl библиотеки GL. Для использования всех этих методов в приложении можно в библиотеке GL описать дополнительный статический класс listMaker, в котором размещать необходимые методы по созданию дисплейных списков команд OpenGL. Если в этот класс поместить метод, создающий дисплейные списки битовых карт символов заданного шрифта, то соответствующий код будет выглядеть следующим образом 67 /// <summary> /// Состоит из статических методов, создающих дисплейные списки команд OpenGL /// </summary> public static class listMaker { /// <summary> /// Создает 256 списков битовых карт символов (глифов) заданного типа шрифта /// для вывода в заданный контекст устройства. /// </summary> /// <param name="font"> /// Объект используемого шрифта. /// </param> /// <param name="deviceContext"> /// Контекст устройства. /// </param> /// <param name="glyphsBaseList"> /// Возвращаемый номер списка первого символа. /// </param> public static void BMPGlyphs(Font font, int deviceContext, ref uint glyphsBaseList) { // Если список не создан if (0 == glyphsBaseList) // Создается 256 непрерывно расположенных номеров пустых дисплейных списков, // первый номер которых возвращается в glyphsBaseList, // определяя базовый номер всех списков glyphsBaseList = gl.GenLists(256); // Новый шрифт временно заменяет существующий шрифт контекста устройства IntPtr oldFont = gl.SelectObject(deviceContext, font.ToHfont()); // Создаются списки битовых карт символов от 0 до 255 // Нумерация списков начинается с gliphsBaseList if (!gl.UseFontBitmaps(deviceContext, 0, 256, glyphsBaseList)) throw new Exception("Списки битовых карт символов не установлены."); // Прежний шрифт возвращается контексту gl.SelectObject(deviceContext, oldFont); } } Класс Font описан в системной библиотеке System.Drawing. Поэтому необходимо добавить эту библиотеку к ресурсам библиотеки GL (окно Solution Explorer -> узел References библиотеки GL -> команда Add Reference… из контекстного меню -> закладка .NET открывшегося окна Add Reference, найти System.Drawing) и в заголовке файла GL.cs добавить директиву using System.Drawing. Метод BMPGlyphs можно вызывать непосредственно в любом из методов формы для создания и обновления действующего списка битовых карт символов. Алгоритм использования метода BMPGlyphs аналогичен уже рассмотренному алгоритму создания и вызова списка точек окружности. Внутри метода BuildFrame можно набрать следующий код // Устанавливается текущий шрифт символов using (Font curFont = new Font(ppBMPText.bmpFont.FontFamily, ppBMPText.bmpFont.Size)) if (ppBMPText.IsChanged || curBMPGlyphsBaseList == 0) listMaker.BMPGlyphs(curFont, port.deviceContext, ref curBMPGlyphsBaseList); 68 // Флаг изменения параметров очищается curBMPFontChanged = false; // Устанавливается базовый номер списков символов gl.ListBase(curBMPGlyphsBaseList); // Устанавливается текущий цвет символов gl.Color(curBMPFontClrRed, curBMPFontClrGreen, curBMPFontClrBlue); // Устанавливается позиция в окне, куда следует выводить символы gl.RasterPosf(curBMPFontPosX, curBMPFontPosY, curBMPFontPosZ); // Вызываются списки символов, необходимые для воспроизведения текущего текста gl.CallLists(curBMPFontText.Length, gl.UNSIGNED_BYTE, curBMPFontText); В этом коде есть поле curBMPGlyphsBaseList, которое следует обнулять в методе InitSceneAttributes. Другие поля curBMPFont, curBMPFontChanged, curBMPFontClrRed, curBMPFontClrGreen, curBMPFontClrBlue, curBMPFontPosX, curBMPFontPosY, curBMPFontPosZ, curBMPFontText являются переменными, которые регулируются либо пользователем через интерфейсные управляющие элементы, как в иллюстрирующем приложении, либо любым другим способом. Ссылка на код и комментарии для того, кто строит авторское иллюстрирующее приложение. Тест рубежного контроля 70. Для чего используется метод ListBase? 71. Как сохранить в стеке значение текущего базового номера списка? 72. Как узнать значение текущего базового номера списка? 73. Какой метод применяется для вызова последовательности дисплейных списков? 74. Какой метод используется для создания дисплейных списков битовых карт символов? 75. Что устанавливает метод RasterPos? 76. Какую роль играет вызов метода SelectObject при формировании списков битовых карт символов? 11 Контурный шрифт Третий способ вывода символов позволяет изображать символы в виде объемных фигур. Это так называемый контурный шрифт, где каждый символ является отдельным 3-мерным объектом. Метод UseFontOutline Для создания символов контурного шрифта используется метод UseFontOutlines со следующим заголовком /// <summary> /// Создает списки контурных символов. /// </summary> /// <param name="deviceContext"> /// Контекст устройства контурного шрифта. /// </param> /// <param name="first"> /// Номер первого символа, обращаемого в список. /// </param> /// <param name="count"> /// Количество символов, обращаемых в списки. /// </param> /// <param name="listBase"> /// Номер первого списка. /// </param> 69 /// <param name="deviation"> /// Определяет максимально допустимое отклонение от верного контура. /// </param> /// <param name="extrusion"> /// Глубина (толщина) символа в отрицательном направлении оси z. /// </param> /// <param name="format"> /// Указывает тип сегментов контура символа /// контурный (FONT_LINES) или заполненный (FONT_POLYGONS). /// </param> /// <param name="glyphs"> /// Адрес буфера (массива метрических записей), /// в который передаются метрические данные символов. /// </param> /// <returns> /// true при успешном завершении. /// </returns> [DllImport("OPENGL32.DLL", EntryPoint = "wglUseFontOutlines")] public static extern bool UseFontOutlines (int deviceContext, uint first, uint count, uint listBase, float deviation, float extrusion, int format, GLYPHMETRICSFLOAT[] glyphs); Метод UseFontOutlines использует в качестве одного из аргументов одну из символьных постоянных /// <summary> /// Постоянные формата контурного шрифта. Используются при вызове UseFontOutlines. /// </summary> public const int FONT_LINES = 0; public const int FONT_POLYGONS = 1; Каждый из символов контурного шрифта характеризуется атрибутами, собранными в структуру типа /// <summary> /// Тип структуры, хранящей информацию о размерах, /// положении и внешнем виде контурных символов. /// Массив объектов этой структуры является одним из параметров метода UseFontOutlines. /// </summary> [StructLayout(LayoutKind.Sequential)] public struct GLYPHMETRICSFLOAT { public float gmfBlackBoxX; public float gmfBlackBoxY; public float gmfptGlyphOriginX; public float gmfptGlyphOriginY; public float gmfCellIncX; public float gmfCellIncY; } Эти описания следует добавить к классу gl библиотеки GL. Для создания списков символов контурного шрифта можно поместить в класс listMaker библиотеки GL следующий метод /// <summary> 70 /// Создает 256 списков 3D-объектов символов контурного шрифта. /// </summary> /// <param name="font"> /// Объект используемого шрифта. /// </param> /// <param name="metrics"> /// Массив возвращаемых записей метрик символов. /// </param> /// <param name="deviation"> /// Максимальное отклонение шрифта от гладкости /// </param> /// <param name="extrusion"> /// Глубина шрифта в отрицательном направлении оси Z /// </param> /// <param name="format"> /// Формат выводимых символов FONT_LINES или FONT_POLYGONS . /// </param> /// <param name="deviceContext"> /// Контекст устройства. /// </param> /// <param name="outlineGlyphsBaseList"> /// Возвращаемый номер первого списка. /// </param> public static void OutlineGlyphs(Font font, gl.GLYPHMETRICSFLOAT[] metrics, float deviation, float extrusion, int format, int deviceContext, ref uint outlineGlyphsBaseList) { // Новый шрифт временно заменяет существующий шрифт контекста устройства IntPtr oldFont = gl.SelectObject(deviceContext, font.ToHfont()); // Если список не создан if (0 == outlineGlyphsBaseList) // Создается 256 непрерывно расположенных номеров пустых дисплейных списков, // первый номер которых возвращается в outlineGlyphsBaseList, // определяя базовый номер всех списков outlineGlyphsBaseList = gl.GenLists(256); // Создаются списки символов от 0 до 255 контурного шрифта // Нумерация списков начинается с outlineBaseList if (!gl.UseFontOutlines(deviceContext, 0, 256, outlineGlyphsBaseList, deviation, extrusion, format, metrics)) throw new Exception("Списки символов контурного шрифта не установлены."); // Прежний шрифт возвращается контексту gl.SelectObject(deviceContext, oldFont); } При вызове этого метода используется поле fMetrics, которое следует описать в виде /// <summary> /// Хранит массив метрических свойств символов /// </summary> gl.GLYPHMETRICSFLOAT[] fMetrics = new gl.GLYPHMETRICSFLOAT[256]; В метод BuildFrame теперь можно добавить код алгоритма построения и использования символов контурного шрифта при произвольных атрибутах 71 // Устанавливается текущий шрифт символов using (Font curFont = new Font(curOutlnFont.FontFamily, curOutlnFont.Size)) if (ppOutlineFont.IsChanged || curOutlnGlyphsBaseList == 0) // Списки символов воссоздаются, если параметры изменены listMaker.OutlineGlyphs( curFont, fMetrics, ppOutlineFont.deviation, ppOutlineFont.extrusion, ppOutlineFont.format, port.deviceContext, ref curOutlnGlyphsBaseList); // Флаг изменения параметров очищается curOutlineFontChanged = false; // Устанавливается базовый номер списков символов gl.ListBase(curOutlnGlyphsBaseList); // Устанавливается текущий цвет символов gl.Color(curOutlineFontClrRed, curOutlineFontClrGreen, curOutlineFontClrBlue); // Устанавливается положение, куда следует выводить символы // Матрица предыдущего преобразования заталкивается в стек gl.PushMatrix(); gl.Translate(curOutlineFontPosX, curOutlineFontPosY, curOutlineFontPosZ); gl.Scale(.1f, .1f, .1f); // Вызываются списки символов, необходимые для воспроизведения текущего текста gl.CallLists(curOutlineFontText.Length, gl.UNSIGNED_BYTE, curOutlineFontText); // Бывшая матрица преобразования возвращается на поверхность стека, // выталкивая последнюю матрицу. gl.PopMatrix(); Здесь номер текущего списка curOutlnGlyphsBaseList следует обнулять в методе InitSceneAttributes. Поля curOutlnFont, curOutlineFontChanged, curOutlineFontdeviation, curOutlineFontextrusion, curOutlineFontformat, curOutlineFontClrRed, curOutlineFontClrGreen, curOutlineFontClrBlue, curOutlineFontPosX, curOutlineFontPosY, curOutlineFontPosZ и curOutlineFontText должны выбираться пользователем через интерфейсные контрольные элементы, как это сделано в авторском приложении, либо задаваться другим способом. Ссылка на код и комментарии для того, кто строит авторское иллюстрирующее приложение. Примечание При вызове списков символов контурного шрифта может оказаться необходимым поместить этот вызов внутрь пары PushAttrib/PopAttrib вида gl.PushAttrib(gl.POLYGON_BIT); gl.CallLists(curOutlineFontText.Length, gl.UNSIGNED_BYTE, curOutlineFontText); gl.PopAttrib(); Здесь константа POLYGON_BIT определена следующим образом /// <summary> /// Маска атрибутов многоугольника. /// </summary> public const int POLYGON_BIT = 0x00000008; Методы PushMatrix, PopMatrix В приведенном выше фрагменте кода используются методы PushMatrix и PopMatrix. public static extern void PopMatrix(); /// <summary> /// Проталкивает верхнюю матрицу в стек. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glPushMatrix")] public static extern void PushMatrix(); 72 /// <summary> /// Выталкивает верхнюю матрицу из стека. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glPopMatrix")] Эти описания следует поместить в класс gl библиотеки GL. Методы PushMatrix/PopMatrix работают так же, как методы PushAttrib/PopAttrib. Метод PushMatrix заталкивает в стек матриц копию матрицы предыдущего преобразования. Далее вводимые матрицы умножаются на предыдущую, но копия предыдущей матрицы остается нетронутой в глубине стека. После окончания формирования изображения с требуемым преобразованием матрица, сохраненная в стеке, возвращается на его поверхность методом PopMatrix. Проектное задание Проведите необходимые редакции кода в своей, либо в авторской версии проекта, достаточные для устойчивой работы кода. Тест рубежного контроля 77. Какой метод используется при создании списков контурного шрифта? 78. Какой смысл имеет параметр deviation метода создания списков контурного шрифта? 79. Какой смысл имеет параметр extursion метода создания списков контурного шрифта? 80. Какой смысл имеет параметр format метода создания списков контурного шрифта? 81. Какую функцию выполняет метод PushMatrix? 82. Какой смысл имеет метод PopMatrix? Модуль 1. Прямые линии Комплексной целью модуля является изучение следующих элементов OpenGL Примитив "прямая линия". Типы примитивов, состоящих из прямых линий – отдельные отрезки прямых, отрезки прямых, соединенные своими концами, замкнутые фигуры из отрезков прямых линий. Атрибуты прямых линий. Подобно точкам прямые линии обладают шириной, сглаживаемостью, цветом. Кроме того прямые линии могут быть прерывистыми. 1.0 Примитив LINES Примитив LINES позволяет изображать отдельные отрезки прямых линий, концы которых определены вершинами, перечисленными между вызовами методов Begin/End. Отрезки прямых линий могут иметь разную толщину, цвет, сглаживаемость, быть сплошными или прерывистыми. Первый примитив, имеющий отношение к прямым линиям, представляет собой отдельные отрезки прямых, заданные своими граничными вершинами. Перечисление вершин проводится внутри пары gl.Begin(gl.LINES); gl.End(); Константа LINES уже описана в классе gl метода GL /// <summary> /// Рассматривает каждую пару вершин как независимый отрезок прямой. /// Вершины 2n - 1 и 2n определяют прямую n. /// Изображается N/2 прямых, где N - полное число вершин. /// </summary> public const int LINES = 0x0001; Атрибутами прямых линий управляют методы, которые рассмотрены в следующих разделах. 73 Метод LineWidth Ширина линии определяется методом LineWidth /// <summary> /// Устанавливает ширину линии width /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glLineWidth")] public static extern void LineWidth(float width); При сохранении атрибутов прямой линии в стеке следует использовать метод PushAttrib с константой /// <summary> /// Маска атрибутов прямой. /// </summary> public const int LINE_BIT = 0x00000004; Для определения текущих значений ширины линии, ее минимального, максимального значений и шага, с которым ширина линии меняется, следует использовать метод Get в форме // Минимальная ширина линии gl.Get(gl.LINE_WIDTH_RANGE, 2)[0]; // Максимальная ширина линии gl.Get(gl.LINE_WIDTH_RANGE, 2)[1]; // Шаг изменения ширины линии gl.Get(gl.LINE_WIDTH_GRANULARITY, 1)[0]; // Текущая ширина линии gl.Get(gl.LINE_WIDTH, 1)[0]; Константы, используемые здесь, имеют значения /// <summary> /// Аргумент Get требует возврата значения двух параметров /// минимально и максимально возможной ширины прямой линии. /// </summary> public const int LINE_WIDTH_RANGE = 0x0B22; /// <summary> /// Аргумент Get требует возврата значения одного параметра /// раличия в значениях ширины линии, которые учитываются в изображении. /// </summary> public const int LINE_WIDTH_GRANULARITY = 0x0B23; /// <summary> /// Аргумент Get требует возврата значения одного параметра /// текущей ширины линии. /// </summary> public const int LINE_WIDTH = 0x0B21; Сглаживаемость линии Подобно точке изображение линии может регулировать эффект сглаживаемости. Этот эффект управляется методом Enable с параметром /// <summary> /// Параметр сглаживания изображения линии. /// Используется методами Get, Enable, IsEnabled и Disable. /// </summary> public const int LINE_SMOOTH = 0x0B20; 74 Узнать текущее состояние активности сглаживания линии можно либо методом Get, либо методом IsEnable с тем же параметром. Примечание Активизация сглаживаемости в случае прямой линии может быть не эффективна, если толщина линии достаточно велика. Поэтому использование этого атрибута лучше ограничить только малыми толщинами. Метод LineStipple Линии можно изображать пунктиром произвольного формата. Формат пунктира определяется параметрами метода LineStipple /// <summary> /// Определяет шаблон пунктира при изображении прямой линии /// </summary> /// <param name="factor"> /// Число повторов каждого бита шаблона при изображении пунктира. /// принимает значения от 1 до 256. По умолчанию равен 1. /// </param> /// <param name="pattern"> /// 16-битовый шаблон пунктира. Те части линии, где в бите шаблона находится 1, /// будут выводиться на экран, остальные нет. /// По умолчанию все биты равны единице, и шаблон равен 65535 - сплошная линия. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glLineStipple")] public static extern void LineStipple(int factor, ushort pattern); Текущее состояние параметров factor и pattern определяется методом Get в следующей форме // Текущее значение фактора прерывистости (int)gl.Get(gl.LINE_STIPPLE_REPEAT, 1)[0] // Текущее значение маски прерывистости (int)gl.Get(gl.LINE_STIPPLE_PATTERN, 1)[0] Использованные здесь константы равны /// <summary> /// Аргумент Get требует возврата значения одного параметра /// фактора прерывистости прямой линии. /// </summary> public const int LINE_STIPPLE_REPEAT = 0x0B26; /// <summary> /// Аргумент Get требует возврата значения одного параметра /// шаблона прерывистости прямой линии. /// </summary> public const int LINE_STIPPLE_PATTERN = 0x0B25; Метод LineStipple эффективен лишь в том случае, если активировано пунктирное изображение. Активация проводится методом Enable с параметром /// <summary> /// Параметр активации пунктирного изображения прямой линии /// </summary> public const int LINE_STIPPLE = 0x0B24; Определение текущей активности пунктирного изображения проводится либо методом Get, либо методом IsEnable с тем же параметром. 75 Используйте описанные методы в своем приложении. Посмотрите иллюстрацию использования этих методов в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 1. Какой метод устанавливает ширину линии? 2. Как получить текущие значения ширины линии, ее минимального значения, максимального значения и шага изменения? 3. Что делает метод LineStipple? 4. Как получить текущие значения параметров метода LineStipple? 5. Как сохранить текущие атрибуты линии в стеке? 6. Как активировать прерывистость линии? Проектные задания 1. Напишите алгоритм, который позволит изобразить оси объектной системы координат с метками в виде букв X, Y, Z у положительных окончаний соответствующих осей тех же цветов, используя символы в форме битовых карт. 2. Создайте дисплейный список команд, включив в него команды созданного алгоритма. 3. Изобразите метки осей в форме букв контурного шрифта (третий способ) так, чтобы символы имели цвет оси, находились на конце оси и лежали в плоскости перпендикулярной оси. Указание: используйте методы PushMatrix, PopMatrix, Translate, Rotate, Scale с каждым из символов. 1.1 Примитивы LINE STRIP и LINE LOOP Второй примитив прямых линий LINE_STRIP определяет последовательность связанных друг с другом отрезков прямых /// <summary> /// Изображает связанную группу отрезков прямых от первой до последней вершины. /// Вершины n и n+1 определяют прямую n. Изображается N - 1 прямая, где N - полное число вершин. /// </summary> public const int LINE_STRIP = 0x0003; Вершины указываются между методами gl.Begin(gl.LINE_STRIP); gl.End(); Каждой вершине может соответствовать свой цвет, как и в случае отдельных отрезков прямых линий. Третий примитив линии LINE_LOOP изображает замкнутую фигуру из отрезков прямых. /// <summary> /// Изображает связанную группу отрезков прямых от первой до последней вершины, /// затем назад - к первой вершине. /// Вершины n и n+1 определяют прямую n. Последняя прямая определяется вершинами N и 1. /// Изображается N прямых, где N - полное число вершин. /// </summary> public const int LINE_LOOP = 0x0002; Достаточно заменить в предыдущем фрагменте кода параметр gl.LINE_STRIP в методе gl.Begin значением gl.LINE_LOOP. Конец последнего отрезка прямой будет совпадать с началом первого – фигура образует многоугольник. Посмотрите иллюстрацию использования этих методов в авторском приложении. Код этой части авторского приложения можно найти по ссылке. 76 Вопрос для самоконтроля Чем отличаются примитивы LINE_STRIP и LINE_LOOP? Проектные задания 1. Сделайте надпись "LINE LOOP" с помощью контурного шрифта в плоскости треугольника из отрезков прямых. 2. Постройте окружность единичного радиуса из отрезков прямых линий, лежащую в плоскости XY, используя примитив LINE_LOOP. 3. С помощью символов контурного шрифта изобразите циферблат. 4. Изобразите минутную и секундную стрелки на циферблате и, используя системный таймер, заставьте часы идти. Модуль 2. Треугольники Комплексной целью модуля является изучение следующих элементов OpenGL Примитив "треугольник". Типы примитивов, состоящих из треугольников – отдельные треугольники, треугольники, имеющие общие стороны и треугольники, имеющие общие вершины. Атрибуты треугольников гораздо богаче атрибутов точек и прямых линий. Треугольники имеют грани, которые могут сплошными и контурными. К сторонам треугольников применимы атрибуты прямых линий, а к его вершинам – атрибуты точек. Сплошная грань может использовать произвольный шаблон закрашивания. Используя треугольники можно сформировать практически произвольную поверхность. Здесь приводятся примеры сферы и тора. 2.0 Примитив "TRIANGLES" Примитив, управляемый параметром TRIANGLES при вызове метода Begin, строит отдельные изображения треугольников. Код, который строит отдельные треугольники должен иметь вид. gl.Begin(gl.TRIANGLES); // Вершины определяются здесь gl.End(); Константа /// <summary> /// Рассматривает каждый триплет вершин, как независимый треугольник. /// Вершины 3n - 2, 3n-1 и 3n определяют треугольник n. /// Изображается N/3 треугольников, где N - полное число вершин. /// </summary> public const int TRIANGLES = 0x0004; Рисунок, иллюстрирующий порядок построения отдельных треугольников по заданным вершинам в примитиве TRIANGLES 77 В отличие от прямых линий у треугольника, как и у любого многоугольника, есть поверхность, ограниченная краями. Поверхностей даже две – передняя, или фронтальная грань, и задняя грань. То, какая из граней фронтальная, регулируется методом FrontFace. Метод FrontFace Фронтальность грани определяется правилом обхода сторон, которое, в свою очередь, задается последовательностью, в которой перечисляются вершины многоугольника и, в частности, треугольника. Правило обхода определяется в общем случае знаком выражения xwiywi+1 – xwi+1ywi, где по i производится суммирование от 0 до n-1 для n вершин, причем вершина номер n совпадает с вершиной 0, а xw, yw – оконные координаты вершин. Если знак положительный, то по умолчанию (так называемый обход против часовой стрелки – ориентация counter- clockwise, или CCW) грань является фронтальной, если отрицательный – то это задняя грань. Метод FrontFace может менять это соответствие на обратное /// <summary> /// Устанавливает правило определения фронтальной грани многоугольника. /// </summary> /// <param name="mode"> /// Правило обхода вершин, определяющее фронтальную грань. /// CW либо CCW (по умолчанию). /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glFrontFace")] public static extern void FrontFace(int mode); Аргументами этого метода являются символьные постоянные // Аргументы метода FrontFace - CW (clockwise - по часовой стрелке) и // CCW (counter-clockwise - против часовой стрелки). public const int CW = 0x0900; public const int CCW = 0x0901; Текущее значение параметра метода FrontFace возвращает метод Get в следующей форме (int)gl.Get(gl.FRONT_FACE, 1)[0] Здесь используется символьная константа /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// символьной постоянной CCW или CW, параметра метода FrontFace. /// </summary> public const int FRONT_FACE = 0x0B46; Для сохранения в стеке текущего правила обхода следует использовать метод PushAttrib с параметром POLYGON_BIT, уже встречавшемся выше. Этот же параметр используется для сохранения в стеке других атрибутов многоугольника, описанных в настоящем разделе, за исключением маски заполнения граней (см. ниже). Любую из граней – заднюю или переднюю можно маскировать от воспроизведения в буфере фрейма. Для этого используется метод CullFace. Метод CullFace /// <summary> /// Маскирует изображение стороны многоугольника. /// </summary> /// <param name="mode"> 78 /// Сторона многоугольника FRONT или BACK (по умолчанию). /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glCullFace")] public static extern void CullFace(int mode); Константы, используемые в качестве параметра метода CullFace, имеют вид // Константы, используемые для определения типа грани. public const int FRONT = 0x0404; public const int BACK = 0x0405; Текущее значение этого параметра метода CullFace определяется методом Get в следующей форме (int)gl.Get(gl.CULL_FACE_MODE, 1)[0] Здесь параметром является константа /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// символьной постоянной BACK или FRONT, параметра метода CullFace. /// </summary> public const int CULL_FACE_MODE = 0x0B45; Метод CullFace полезно использовать в том случае, когда изображается замкнутая фигура, внутренние грани которой не видны и на их изображение нет смысла тратить ресурсы. Для активации метода CullFace требуется вызов метода Enable с параметром CULL_FACE /// <summary> /// Параметр активации метода CullFace. Используется в методах Enable, Disable, IsEnabled, Get /// </summary> public const int CULL_FACE = 0x0B44; После активации по умолчанию маскируется задняя грань. Для маскировки передней грани необходимо явно вызвать метод CullFace с параметром FRONT. Определить текущее состояние активности метода CullFace можно методом IsEnabled или Get с тем же параметром CULL_FACE. Метод PolygonMode При отображении многоугольников по умолчанию обе грани многоугольника заполняются сплошным образом пикселями, цвет которых определяется цветами вершин. Существует метод PolygonMode, позволяющий регулировать выбор между сплошным заполнением любой из граней, отображением только контура грани, или отображением только вершин, ограничивающих контур. При этом каждая из граней может иметь свой режим отображения, а атрибуты отрезков прямых линий контура и отдельных точек при отображении вершин можно регулировать так же, как это делалось в соответствующих примитивах (линии, точки). Так выглядит заголовок метода PolygoneMode /// <summary> /// Устанавливает режим отображения многоугольников. /// </summary> /// <param name="face"> /// Определяет, какая из граней многоугольника отображается в заданном режиме /// FRONT, BACK или FRONT_AND_BACK. /// </param> /// <param name="mode"> /// Определяет режим отображения многоугольника POINT, LINE или FILL. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glPolygonMode")] public static extern void PolygonMode(int face, int mode); 79 Константы, являющиеся аргументами метода PolygonMode, имеют вид (константы типа грани FRONT и BACK описаны выше) // Тип грани (по умолчанию это тип, который установлен) public const int FRONT_AND_BACK = 0x0408; // Константы режима воспроизведения /// <summary> /// Отображаются только вершины выбранной грани многоугольника /// отдельными точками с их атрибутами (размером и сглаживанием). /// </summary> public const int POINT = 0x1B00; /// <summary> /// Отображаются только контуры многоугольника отрезками прямых линий /// с их атрибутами (толщина, сглаживание и прерывистость). /// </summary> public const int LINE = 0x1B01; /// <summary> /// Грань многоугольника заполняется сплошным образом (по умолчанию) с учетом маски, /// регулируемой методом PolygonStipple. /// </summary> public const int FILL = 0x1B02; Текущие значения параметров метода PolygonMode определяются методом Get в следующей форме (int)gl.Get(gl.POLYGON_MODE, 2)[i] Здесь в элементе i = 0 возвращается режим заполнения фронтальной грани многоугольника, а в элементе i=1 – задней грани. Константа POLYGON_MODE имеет вид /// <summary> /// Аргумент Get требует возврата значений двух параметров /// символьной константы режима заполнения фронтальной и задней граней многоугольника. /// </summary> public const int POLYGON_MODE = 0x0B40; В режиме FILL грань заполняется по умолчанию сплошным образом. Однако, как и в случае изображения линий, существует возможность маскировать вывод в буфер фрейма отдельных пикселей, строя прерывистое изображение грани. Маску заполнения регулирует метод PolygonStipple. Метод PolygonStipple /// <summary> /// Определяет шаблон наполнителя поверхности многоугольника /// </summary> /// <param name="mask"> /// Массив байтов, задающий маску шаблона, размером 128 байт. /// По умолчанию все биты каждого байта заполнены единицами – сплошное заполнение. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glPolygonStipple")] public static extern void PolygonStipple(byte[] mask); Текущее значение маски определяется методом GetPolygonStipple /// <summary> /// Возвращает текущее значение маски заполнения при отображении граней многоугольника. /// </summary> 80 /// <param name="mask"> /// Массив маски объемом 128. Содержит квадрат 32x32 бита с маской заполнения. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glGetPolygonStipple")] public static extern void GetPolygonStipple(byte[] mask); Для сохранения в стеке состояние маски заполнения грани многочлена следует использовать метод PushAttrib с параметром /// <summary> /// Маска битов образа заполнения многоугольника. /// </summary> public const int POLYGON_STIPPLE_BIT = 0x00000010; Для активации метода PolygonStipple следует использовать метод Enable с параметром /// <summary> /// Параметр активации шаблона наполнителя многоугольника. /// Аргумент методов Enable, Disable, IsEnabled и Get. /// </summary> public const int POLYGON_STIPPLE = 0x0B42; Тот же аргумент используют методы IsEnabled и Get для определения текущего состояния активации PolygonStipple. Читатель, строящий собственное приложение, может добавить в него описанные методы в применении к треугольнику и посмотреть результат их действия. Посмотрите иллюстрацию использования этих методов в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 1. Какая постоянная при вызове Begin определяет примитив изображения отдельных треугольников? 2. Как определяется фронтальная грань многоугольника по умолчанию? 3. Какой метод устанавливает правило фронтальной грани? 4. Как возвратить текущее значение фронтальной грани? 5. Какой параметр метода PushAttrib сохраняет значение фронтальной грани в стеке? 6. Как активировать маскировку грани многоугольника? 7. Изображение какой грани маскируется по умолчанию? 8. Какой метод устанавливает маскируемую грань? 9. Как определить текущее значение маскируемой грани? 10. Какие режимы воспроизведения граней многоугольника существуют? 11. Какой метод устанавливает режимы воспроизведения граней? 12. Как определить текущий режим воспроизведения граней многоугольника? 13. Как активировать использование шаблона наполнителя грани? 14. Какой метод устанавливает маску шаблона наполнителя грани многоугольника? 15. Что представляет собой маска шаблона наполнителя грани многоугольника? 16. Какой метод возвращает текущую маску наполнителя граней? 17. Какое значение по умолчанию имеет маска наполнителей граней многоугольника? 18. Какой параметр метода PushAttrib сохраняет в стеке маску наполнителя грани многоугольника? 2.1 Примитивы TRIANGLE STRIP и TRIANGLE FAN Примитивы, управляемые параметрами TRIANGLE_STRIP и TRIANGLE_FAN при вызове метода Begin, строят связанные изображения треугольников. Ознакомьтесь внимательно с комментариями к определению этих постоянных /// <summary> 81 /// Изображает связанную группу треугольников. /// Один треугольник определен для каждой вершины после первых двух вершин. /// Для нечетных n, вершины n, n+1 и n+2 определяют треугольник n. /// Для четных n, вершины n+1, n и n+2 определяют треугольник n. /// Изображается N - 2 треугольника, где N - полное число вершин. /// </summary> public const int TRIANGLE_STRIP = 0x0005; /// <summary> /// Изображает связанную группу треугольников. /// Один треугольник определен для каждой вершины после первых двух вершин. /// Вершины 1, n+1 и n+2 определяют треугольник n. /// Изображается N - 2 треугольника, где N - полное число вершин. /// </summary> public const int TRIANGLE_FAN = 0x0006; Эти константы должны являться аргументами метода Begin при построении примитивов TRIANGLE_STRIP и TRIANGLE_FAN. Рисунки, иллюстрирующие порядок перечисления вершин при построении треугольников с помощью примитивов TRIANGLE_STRIP TRIANGLE_FAN Читатель, строящий собственное приложение, может добавить в него построения описанных примитивов на примере двух, или большего числа треугольников и посмотреть результат их работы. Посмотрите иллюстрацию использования этих методов в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Вопрос для самоконтроля Чем отличаются примитивы TRIANGLE_STRIP и TRIANGLE_FAN? Проектные задания 1. Напишите список команд, строящих треугольную пирамиду с основанием и без него. 2. Используя примитивы "TRIANGLE_FAN", создайте метод createDiskList построения дисплейного списка диска единичного радиуса в плоскости XY и произвольным числом slices (параметр метода) треугольников. 3. Используя примитив "TRIANGLE_FAN", создайте метод createConeList с построением дисплейного списка из slices (параметр метода) треугольников кругового конуса единичной высоты и радиуса, с вершиной в начале координат и осью, направленной в положительном направлении оси z. 2.2 Дисплейные списки сферы и тора В этом разделе даются два примера использования примитивов TRIANGLE_STRIP и TRIANGLE_FAN. В одном строится дисплейный список сферы, в другом - тора. 82 Сфера Следующий метод можно добавить в класс listMaker библиотеки GL. Обратите внимание на то, как используются примитивы TRIANGLE_STRIP и TRIANGLE_FAN при создании списка команд, изображающих сферу. /// <summary> /// В режиме COMPILE создает команды дисплейного списка, если список не создан; /// в режиме COMPILE_AND_EXECUTE создает и, если список создан, /// вызывает команды дисплейного списка /// сферы единичного радиуса с осью вдоль оси y. /// Список строится из примитивов TRIANGLE_FAN и TRIANGLE_STRIP. /// </summary> /// <param name="mode"> /// Режим создания списка COMPILE или COMPILE_AND_EXECUTE /// </param> /// <param name="slices"> /// Число сегментов вдоль оси (по долготе). /// </param> /// <param name="stacks"> /// Число сегментов, перпендикулярных оси (по широте). /// </param> /// <param name="alpha"> /// Альфа-компонента цвета /// </param> /// <param name="sphereList"> /// Номер возвращаемого дисплейного списка. /// </param> public static void Sphere(int mode, int slices, int stacks, float alpha, ref uint sphereList) { // Если список создан, то он вызывается if (0 != sphereList) { gl.CallList(sphereList); return; } // Текущий цвет (рабочая переменная) float curColor; // Тригонометрические функции углов по широте и долготе (рабочие переменные) float[] sinteta = new float[stacks], costeta = new float[stacks], sinfi = new float[slices + 1], cosfi = new float[slices + 1]; // Инициализация тригонометрических функций for (int slice = 0; slice <= slices; slice++) { sinfi[slice] = (float)Math.Sin(2.0 * Math.PI * slice / slices); cosfi[slice] = (float)Math.Cos(2.0 * Math.PI * slice / slices); } for (int stack = 0; stack < stacks; stack++) 83 { sinteta[stack] = (float)Math.Sin(Math.PI * stack / stacks); costeta[stack] = (float)Math.Cos(Math.PI * stack / stacks); } // Генерируется свободный номер для списка сферы sphereList = gl.GenLists(1); // Создается и выполняется дисплейный список gl.NewList(sphereList, mode); gl.PushAttrib(gl.CURRENT_BIT); // Северный полюс gl.Begin(gl.TRIANGLE_FAN); // Цвет вблизи полюса - синий gl.Color(0, 0, 1, alpha); gl.Vertex(0, 1, 0); for (int slice = 0; slice <= slices; slice++) { gl.Vertex(sinteta[1] * sinfi[slice], costeta[1], sinteta[1] * cosfi[slice]); } gl.End(); // средняя часть сферы for (int stack = 1; stack < stacks - 1; stack++) { // Алгоритм изменения цвета с широтой: от синего до красного и назад - к синему if (stack < stacks / 4) gl.Color(0, curColor = 4.0f * stack / stacks, 1.0f - curColor, alpha); else if (stack < stacks / 2) gl.Color(curColor = 4.0f * (stack - stacks / 4) / stacks, 1.0f - curColor, 0, alpha); else if (stack < 3 * stacks / 4) gl.Color(curColor = 1 - 4.0f * (stack - stacks / 2) / stacks, 1.0f - curColor, 0, alpha); else gl.Color(0, curColor = 1 - 4.0f * (stack - 3 * stacks / 4) / stacks, 1.0f - curColor, alpha); // Геометрия средней части gl.Begin(gl.TRIANGLE_STRIP); for (int slice = 0; slice <= slices; slice++) { gl.Vertex(sinteta[stack] * sinfi[slice], costeta[stack], sinteta[stack] * cosfi[slice]); gl.Vertex(sinteta[stack + 1] * sinfi[slice], costeta[stack + 1], sinteta[stack + 1] * cosfi[slice]); } gl.End(); } // Южный полюс 84 gl.Begin(gl.TRIANGLE_FAN); gl.Vertex(0, -1, 0); for (int slice = slices; slice >= 0; slice--) { gl.Vertex(sinteta[stacks - 1] * sinfi[slice], costeta[stacks - 1], sinteta[stacks - 1] * cosfi[slice]); } gl.End(); gl.PopAttrib(); gl.EndList(); } Тор Можно добавить в класс listMaker метод, создающий дисплейный список тора. /// <summary> /// Хранит минимально допустимый малый радиус тора /// </summary> public const float torusMinSmallRadius = .01f; /// <summary> /// В режиме COMPILE создает команды дисплейного списка, если список не создан; /// в режиме COMPILE_AND_EXECUTE создает и, если список создан, /// вызывает команды дисплейного списка тора единичного внешнего радиуса /// и произвольного радиуса сечения из примитивов TRIANGLE_STRIP. /// </summary> /// <param name="mode"> /// Режим создания списка COMPILE или COMPILE_AND_EXECUTE /// </param> /// <param name="bigSlices"> /// Число сегментов на большой окружности тора. /// </param> /// <param name="smallSlices"> /// Число сегментов на малой окружности тора. /// </param> /// <param name="smallRadius"> /// Малый радиус тора. Должен быть в интервале (0; 0.5) /// </param> /// <param name="alpha"> /// Альфа-компонента цвета /// </param> /// <param name="sphereList"> /// Номер возвращаемого дисплейного списка. /// </param> public static void Torus(int mode, int bigSlices, int smallSlices, float smallRadius, float alpha, ref uint torusList) { if (smallRadius <= torusMinSmallRadius || smallRadius >= .5f) throw new Exception( 85 String.Format("Малый радиус тора должен лежать в интервале torusMinSmallRadius)); // Если список создан, то он вызывается if (torusList != 0) { gl.CallList(torusList); return; } // Генерируется свободный номер для списка тора torusList = gl.GenLists(1); // Текущий цвет (рабочая переменная) float curColor, curRad, nextRad; // Тригонометрические функции углов по широте и долготе (рабочие переменные) float[] sinSmallFi = new float[smallSlices + 1], cosSmallFi = new float[smallSlices + 1], sinBigFi = new float[bigSlices + 1], cosBigFi = new float[bigSlices + 1]; // Инициализация тригонометрических функций for (int smallSlice = 0; smallSlice <= smallSlices; smallSlice++) { sinSmallFi[smallSlice] = (float)Math.Sin(2.0 * Math.PI * smallSlice / smallSlices); cosSmallFi[smallSlice] = (float)Math.Cos(2.0 * Math.PI * smallSlice / smallSlices); } for (int bigSlice = 0; bigSlice <= bigSlices; bigSlice++) { sinBigFi[bigSlice] = (float)Math.Sin(2.0 * Math.PI * bigSlice / bigSlices); cosBigFi[bigSlice] = (float)Math.Cos(2.0 * Math.PI * bigSlice / bigSlices); } // Создается и выполняется дисплейный список gl.NewList(torusList, mode); gl.PushAttrib(gl.CURRENT_BIT); for (int smallSlice = 0; smallSlice < smallSlices; smallSlice++) { curRad = (1 - smallRadius * (1 - cosSmallFi[smallSlice])); nextRad = (1 - smallRadius * (1 - cosSmallFi[smallSlice + 1])); if (smallSlice < smallSlices / 4) gl.Color(curColor = 1.0f - 4.0f * smallSlice / smallSlices, 1.0f - curColor, 0, alpha); else if (smallSlice < smallSlices / 2) gl.Color(0, curColor = 1.0f - 4.0f * (smallSlice - smallSlices / 4) / smallSlices, 1.0f - curColor, alpha); else if (smallSlice < 3 * smallSlices / 4) gl.Color(0, curColor = 4.0f * (smallSlice - smallSlices / 2) / smallSlices, 1.0f - curColor, alpha); else gl.Color(curColor = 4.0f * (smallSlice - 3 * smallSlices / 4) / smallSlices, 1.0f - curColor, 0, alpha); gl.Begin(gl.TRIANGLE_STRIP); ({0}0;0.5).", 86 for (int bigSlice = bigSlices; bigSlice >= 0; bigSlice--) { gl.Vertex(curRad * cosBigFi[bigSlice], curRad * sinBigFi[bigSlice], smallRadius * sinSmallFi[smallSlice]); gl.Vertex(nextRad * cosBigFi[bigSlice], nextRad * sinBigFi[bigSlice], smallRadius * sinSmallFi[smallSlice + 1]); } gl.End(); } gl.PopAttrib(); gl.EndList(); } Читатель, строящий собственное приложение, может добавить в него вызов построенных методов, чтобы проверить их работу. Посмотрите иллюстрацию использования дисплейных списков сферы и тора в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Проектное задание Используя дисплейный список тора, замените окружность циферблата часов на тор с малым "малым радиусом". Модуль 3. 4-угольники Комплексной целью модуля является изучение следующих элементов OpenGL Примитив "4-угольник". Типы примитивов, состоящих из 4-угольников – отдельные 4-гольники, 4-угольники, имеющие общие стороны. Атрибуты 4-угольников не отличаются от атрибутов треугольников. Однако, 4-угольники не обязательно являются плоскими фигурами. Из 4-угольников можно формировать различные поверхности. Приводится пример усеченного конуса. 3.0 Примитив QUADS Примитив QUADS позволяет изображать отдельные 4-угольники. Параметр метода Begin в этом случае имеет вид /// <summary> /// Рассматривает каждую группу из четырех вершин, как независимый четырехугольник. /// Вершины 4n - 3, 4n - 2, 4n - 1 и 4n определяют четырехугольник n. /// Изображается N/4 четырехугольника, где N - полное число вершин. /// </summary> public const int QUADS = 0x0007; Рисунок, иллюстрирующий правило перечисление вершин при построении примитивов QUADS 87 Читатель, строящий собственное приложение, может добавить в него построение одного или нескольких 4-угольников, применив примитив QUADS. Посмотрите иллюстрацию к этому разделу в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Примечание. Если 4-угольник является плоским прямоугольником, то можно воспользоваться методом Rect, имеющимся в библиотеке OpenGL /// <summary> /// Изображает прямоугольник с двумя противоположными вершинами. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glRectf")] public static extern void Rect(float x1, float y1, float x2, float y2); 3.1 Примитив QUAD STRIP Для изображения этого примитива параметром метода Begin должна быть константа /// <summary> /// Изображает связанную группу четырехугольников. /// Один четырехугольник определен для каждой пары вершин после первой пары. /// Вершины 2n - 1, 2n, 2n+2 и 2n+1 определяют четырехугольник n. /// Изображается N четырехугольника, где N - полное число вершин. /// Заметьте, что порядок используемых вершин для построения четырехугольника в данном примитиве /// отличается от того, что используется при построении независимых четырехугольников. /// </summary> public const int QUAD_STRIP = 0x0008; Рисунок, иллюстрирующий порядок перечисления вершин при построении примитива QUAD_STRIP Обратите внимание на порядок перечисления вершин при отображении примитива QUAD_STRIP. Читатель, строящий собственное приложение, может добавить в него построение 2-ух или нескольких 4-угольников, применив примитив QUAD_STRIP. Посмотрите иллюстрацию к этому разделу в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Проектные задания 1. Постройте, применяя QUAD_STRIP, алгоритм изображения кругового цилиндра единичной высоты и радиуса с осью вдоль оси z и основанием на плоскости xy. 2. Добавьте метод createCylinderList, создающий дисплейный список цилиндра cylinderList с произвольным числом поперечных (stacks) и продольных (slices) фрагментов. 3. Сделайте стрелки на часах цилиндрическими, используя дисплейный список цилиндра. 88 3.2 Дисплейный список усеченного конуса Для иллюстрации применения рассмотренных примитивов посмотрите дисплейный список команд, изображающих усеченный конус (Frustum). Можно добавить в класс listMaker метод создания этого списка /// <summary> /// В режиме COMPILE создает команды дисплейного списка, если список не создан; /// в режиме COMPILE_AND_EXECUTE создает и, если список создан, /// вызывает команды дисплейного списка /// кругового усеченного конуса с единичным базовым радиусом, единичной высотой, /// с основанием на плоскости xy и осью вдоль оси z. /// </summary> /// <param name="mode"> /// Режим создания списка COMPILE или COMPILE_AND_EXECUTE /// </param> /// <param name="slices"> /// Число продольных, вертикальных сегментов конуса, нарезанных вдоль оси. /// </param> /// <param name="stacks"> /// Число поперечных, горизонтальных сегментов конуса, нарезанных поперек оси. /// </param> /// <param name="topRadius"> /// Значение радиуса верхней грани конуса по отношению /// к базовому радиусу (в интервале [0;1]). /// </param> /// <param name="alpha"> /// Альфа-компонента цвета /// </param> /// <param name="frustumList"> /// Номер созданного списка. /// </param> public static void Frustum(int mode, int slices, int stacks, float topRadius, float alpha, ref uint frustumList) { if (topRadius < 0 || topRadius > 1) throw new Exception("Параметр topRadius должен лежать в интервале [0;1]."); // Если список создан, то он вызывается if (frustumList != 0) { gl.CallList(frustumList); return; } // Рабочие переменные float curRadius, radiusNext, curColor, x, y, z, xNext, yNext, zNext; // Тригонометрические функции углов (рабочие переменные) float[] sinfi = new float[slices + 1], cosfi = new float[slices + 1]; // Инициализация тригонометрических функций for (int slice = 0; slice <= slices; slice++) { 89 sinfi[slice] = (float)Math.Sin(2.0 * Math.PI * slice / slices); cosfi[slice] = (float)Math.Cos(2.0 * Math.PI * slice / slices); } // Генерируется свободный номер для списка усеченного конуса frustumList = gl.GenLists(1); // Создается и выполняется дисплейный список gl.NewList(frustumList, mode); gl.PushAttrib(gl.CURRENT_BIT); for (int stack = 0; stack < stacks; stack++) { curRadius = 1.0f - stack * (1.0f - topRadius) / stacks; radiusNext = 1.0f - (stack + 1) * (1.0f - topRadius) / stacks; z = 1.0f * stack / stacks; zNext = 1.0f * (stack + 1) / stacks; if (stack < stacks / 2) gl.Color(curColor = 1.0f - 2.0f * stack / stacks, 1.0f - curColor, 0, alpha); else gl.Color(0, curColor = 1.0f - 2.0f * (stack - stacks / 2) / stacks, 1.0f - curColor, alpha); // В случае полного конуса (topRadius=0) последний слой // создается примитивами TRIANGLE_FUN if (topRadius == 0 && stack == stacks - 1) { gl.Begin(gl.TRIANGLE_FAN); gl.Vertex(0, 0, 1); for (int slice = slices; slice >= 0; slice--) { x = curRadius * cosfi[slice]; y = curRadius * sinfi[slice]; gl.Vertex(x, y, z); } gl.End(); } else { gl.Begin(gl.QUAD_STRIP); for (int slice = slices; slice >= 0; slice--) { x = curRadius * cosfi[slice]; y = curRadius * sinfi[slice]; xNext = radiusNext * cosfi[slice]; yNext = radiusNext * sinfi[slice]; gl.Vertex(x, y, z); gl.Vertex(xNext, yNext, zNext); } gl.End(); } } gl.PopAttrib(); gl.EndList(); } 90 Читатель, строящий собственное приложение, может добавить в него построение усеченного конуса и фигур, перечисленных ниже в проектных заданиях. Посмотрите иллюстрацию к этому разделу в авторском приложении. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 1. Опишите соответствие вершин и фигур в примитиве QUADS. 2. Опишите соответствие вершин и фигур в примитиве QUAD_STRIP. Проектные задания 1. Постройте по аналогии с уже рассмотренными методами в классе listMaker библиотеки GL метод public static void Disk(int mode, int slices, int stacks, float intRadius, float alpha, ref uint diskList), создающий список диска diskList с внешним единичным радиусом, лежащим в плоскости xy, с произвольным внутренним радиусом intRadius в интервале [0;1], произвольными числами фрагментов slices и stacks (см., в частности, код конуса) и альфа-компонентой цвета alpha. Проверьте работу метода. 2. Используя дисплейный списки цилиндра и полного конуса, постройте в классе listMaker библиотеки GL метод public static void CylinderAxe(float radius, float pointLength, float pointAngle, char label, uint bmpGlyphsBaseList, ref uint axeList). Этот метод должен создавать дисплейный список стрелки axeList единичной длины, состоящей из стержня-цилиндра радиуса radius, острия с относительной длиной pointLength, половиной угла раствора pointAngle и метки label – символом из списка битовых карт с базовым номером bmpGlyphsBaseList. Если символ не является буквой или цифрой, он не должен изображаться. 3. Замените стрелки в часах подобными стрелками, но без символов. 4. Используя дисплейный список оси CylinderAxe, постройте в классе listMaker библиотеки GL метод изображения осей объектной системы координат с помощью стрелок с названиями осей X, Y, Z. public static void CylinderAxes(uint bmpGlyphsBaseList, ref uint axesList) Этот метод должен создавать список осей координат axesList, используя списки символов битовых карт с базовым номером bmpGlyphsBaseList. 91 Модуль 4. Многоугольники Комплексной целью модуля является изучение следующих элементов OpenGL Примитив многоугольник. Его изображение может существенно зависеть от порядка перечисления вершин. Смешивание цветов. Прозрачность. Отсечение. Работа буфера трафарета. Эффекты буфера аккумулятора. Перспективная проекция. Туман Отсечение изображения в оконных координатах. 4.0 Примитив POLYGON Для изображения примитива POLYGON параметром метода Begin должна быть символьная константа /// <summary> /// Изображает один выпуклый многоугольник. Вершины от 1 до N /// определяют этот многоугольник. /// </summary> public const int POLYGON = 0x0009; В качестве примеров можно построить дисплейный список кардиоиды. Вот так может выглядеть код метода из класса listMaker /// <summary> /// В режиме COMPILE создает команды дисплейного списка, если список не создан; /// в режиме COMPILE_AND_EXECUTE создает и, если список создан, /// вызывает команды дисплейного списка кардиоиды. /// </summary> /// <param name="mode"> /// Режим создания списка COMPILE или COMPILE_AND_EXECUTE /// </param> /// <param name="vertices"> /// Число вершин многоугольника /// </param> /// <param name="cardioidList"> /// Возвращаемый номер списка /// </param> public static void Cardioid(int mode, int vertices, ref uint cardioidList) { double fi; float cosfi, ro; // Если список создан, то он вызывается // Если список создан, то он вызывается if (cardioidList != 0) { gl.CallList(cardioidList); return; 92 } // Генерируется свободный номер для списка кардиоиды cardioidList = gl.GenLists(1); // Создается и выполняется дисплейный список gl.NewList(cardioidList, mode); gl.Begin(gl.POLYGON); for (int i = 0; i < vertices; i++) { fi = 2.0 * Math.PI * i / vertices; cosfi = (float)Math.Cos(fi); ro = 1 + cosfi; gl.Vertex(.75f * ro * (float)Math.Sin(fi), .75f * (-ro * cosfi + 1)); } gl.End(); gl.EndList(); } А это пример дисплейного списка пятиконечной звезды – метод Star класса listMaker. /// <summary> /// В режиме COMPILE создает команды дисплейного списка, если список не создан; /// в режиме COMPILE_AND_EXECUTE создает и, если список создан, /// вызывает команды дисплейного списка пятиконечной звезды в плоскости XY. /// </summary> /// <param name="mode"> /// Режим создания списка COMPILE или COMPILE_AND_EXECUTE /// </param> /// <param name="starList"> /// Возвращаемый номер дисплейного списка звезды. /// </param> public static void Star(int mode, ref uint starList) { // Если список создан, то он вызывается if (starList != 0) { gl.CallList(starList); return; } // Генерируется свободный номер для списка звезды starList = gl.GenLists(1); // Создается и выполняется дисплейный список gl.NewList(starList, mode); gl.Begin(gl.POLYGON); gl.Vertex(-.5f * (float)Math.Sin(1.8 * Math.PI), .5f * (float)Math.Cos(1.8 * Math.PI)); gl.Vertex(0, 1); gl.Vertex(-.5f * (float)Math.Sin(.2 * Math.PI), .5f * (float)Math.Cos(.2 * Math.PI)); gl.Vertex(-(float)Math.Sin(.4 * Math.PI), (float)Math.Cos(.4 * Math.PI)); gl.Vertex(-.5f * (float)Math.Sin(.6 * Math.PI), .5f * (float)Math.Cos(.6 * Math.PI)); gl.Vertex(-(float)Math.Sin(.8 * Math.PI), (float)Math.Cos(.8 * Math.PI)); gl.Vertex(0, -.5f); gl.Vertex(-(float)Math.Sin(1.2 * Math.PI), (float)Math.Cos(1.2 * Math.PI)); 93 gl.Vertex(-.5f * (float)Math.Sin(1.4 * Math.PI), .5f * (float)Math.Cos(1.4 * Math.PI)); gl.Vertex(-(float)Math.Sin(1.6 * Math.PI), (float)Math.Cos(1.6 * Math.PI)); gl.End(); gl.EndList(); } В списке звезды важен порядок, в котором перечисляются вершины. В авторском приложении можно менять этот порядок и наблюдать результирующий эффект. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 1. Какая постоянная при вызове Begin определяет примитив изображения многоугольников? 2. Почему важен порядок перечисления вершин при формировании многоугольника? В OpenGL существуют методы, позволяющие создать эффект прозрачности объектов. В частности, можно сделать так, чтобы сплошная изображаемая фигура стала прозрачной и сквозь нее стали видны оси координат. При этом сохраняется ощущение, что между наблюдателем и осями координат есть другой объект. Достигается это путем смешивания цветов пикселей, находящихся в буфере цвета (destination – адресат, или приемник), с цветами пикселей, туда поступающими (source – источник). 4.1 Смешивание цветов. Прозрачность Смешиванию подвергаются цвета фрагмента, который направляется в буфер цвета (источник, или source) и цвет фрагмента, уже находящегося в том же месте буфера цвета (адресат, приемник, или destination). Существует формула смешивания, в которую входят оба эти цвета с определенными коэффициентами – факторами смешивания. Результирующие цвета становятся цветами фрагмента, помещаемого в буфер цвета. Формула имеет простой вид C = fsCs + fdCd. В ней Cs, Cd – 4-компонентные цвета источника и приемника (например, красный цвет (1,0,0,1) с альфа-компонентой α = 1), fs, fd – так же 4-компонентые факторы смешивания источника и приемника, а C – результирующий цвет. Если смешивание не активировано, результатом является замена цветов фрагмента-приемника цветами фрагмента-источника. Смешивание активируется командой Enable с параметром BLEND. После активации смешивание управляется факторами, которые устанавливаются командой BlendFunc с двумя параметрами фактором-источником и фактором-приемником. По умолчанию цвет источника управляется фактором ONE (fs = (1,1,1,1)) , а цвет приемника – фактором ZERO (fd = (0,0,0,0)). В результате по умолчанию в буфере цвета фрагмент-приемник будет заменен фрагментомисточником, как если бы смешивание не было активировано. Всего в OpenGL существует 9 типов fs - и 8 типов fd -факторов. Все они перечислены в константах, приведенных ниже. В частности, для достижения эффекта прозрачности обычно в качестве фактора источника используется значение SOURCE_ALPHA со значение fs = (αs, αs, αs, αs) , где αs - значение альфакомпоненты цвета фрагмента, направляемого в буфер цвета. Фактор приемника принимается равным ONE_MINUS_ SOURCE_ ALPHA со значением fd = (1-αs, 1-αs, 1-αs, 1-αs). В этом случае, к примеру, красная компонента результирующего цвета будет равна Cr = αs Csr + (1 – αs) Cdr, то есть будет представлять собой смесь красных компонент фрагмента-источника Csr и фрагмента-приемника Cdr. Поместите следующий код в класс gl библиотеки GL. /// <summary> /// Определяет правило смешивания цветов /// </summary> /// <param name="sfactor"> /// Фактор источника /// </param> 94 /// <param name="dfactor"> /// Фактор адресата /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glBlendFunc")] public static extern void BlendFunc(int sfactor, int dfactor); Константы, определяющие факторы смешивания // только s-факторы public const int SRC_ALPHA_SATURATE = 0x0308; public const int DST_COLOR = 0x0306; public const int ONE_MINUS_DST_COLOR = 0x0307; // только d-факторы public const int SRC_COLOR = 0x0300; public const int ONE_MINUS_SRC_COLOR = 0x0301; // s и d-факторы public const int ZERO = 0; public const int ONE = 1; public const int SRC_ALPHA = 0x0302; public const int ONE_MINUS_SRC_ALPHA = 0x0303; public const int DST_ALPHA = 0x0304; public const int ONE_MINUS_DST_ALPHA = 0x0305; Для получения текущих значений s- и d-факторов смешивания следует использовать метод Get в форме (int)gl.Get(gl.BLEND_SRC, 1)[0] (int)gl.Get(gl.BLEND_DST, 1)[0] Константы, использованные в качестве параметров метода Get, в данном случае имеют вид /// <summary> /// Аргумент Get требует возврата значения 1 параметра - текущей символьной константы /// d-фактора смешивания (аргумент BlendFunc) /// </summary> public const int BLEND_DST = 0x0BE0; /// <summary> /// Аргумент Get требует возврата значения 1 параметра - текущей символьной константы /// s-фактора смешивания (аргумент BlendFunc) /// </summary> public const int BLEND_SRC = 0x0BE1; Для активации функции смешивания необходимо использовать метод Enable с параметром /// <summary> /// Параметр активации смешивания цветов (функция BlendFunc) /// </summary> public const int BLEND = 0x0BE2; Для сохранения факторов смешивания и состояния активации смешивания необходимо использовать метод PushAttrib с параметром COLOR_BUFFER_BIT. Читатель может добавить описанные методы в свое приложение и применить их к дисплейным спискам фигур, рассмотренным выше, на фоне осей координат. При этом альфа-компоненты цветов фигур следует задавать меньшими единицы, альфа-компоненту цветов осей оставлять равной единице и вызывать метод BlendFunc с s-фактором, равным SRC_ALPHA, а d-фактором, равным ONE_MINUS_SRC_ALPHA. Здесь важен порядок выполнения списков – вначале необходимо вызвать список осей, чтобы в буфере цвета сформировать пиксели адресата, а затем – список фигуры. 95 В авторском приложении можно включать и выключать смешивания, менять параметры метода BlendFunc и значение альфа-компоненты источника (фигуры), наблюдая результирующий эффект. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 3. В чем принцип смешивания цветов? 4. Чем отличается пиксель-источник от пикселя-адресата? 5. Что определяют константы-факторы источников и адресатов? 6. Какой метод задает функцию смешивания? 7. Как определить текущие значения факторов смешивания цветов? 8. Как сохранить параметры смешивания в стеке атрибутов? 9. Как активировать смешивание цветов? 10. Какие факторы смешивания обеспечивают эффект прозрачности? Известно, что матрица перехода от координат наблюдения к координатам отсечения определяет фактически видимую область изображения. В текущем проекте такая матрица задается методом Ortho с параметрами, выделяющими прямоугольный параллелепипед, границы которого отсекают строящееся изображение. В OpenGL существует возможность отсекать часть изображения, пересекая область видимости произвольными плоскостями, уравнения которых задаются в объектных координатах. Та часть изображения, которая находится над отсекающей плоскостью, остается видимой, та, что под плоскостью, не выводится на экран. Об этом подробнее в следующем разделе. 4.2 Отсечение. Метод ClipPlane В OpenGL существует возможность добиться эффекта отсечения изображения с помощью произвольно задаваемых плоскостей. Эффект отсечения регулируется методом ClipPlane /// <summary> /// Задает плоскость отсечения. /// </summary> /// <param name="plane"> /// Номер плоскости, который должен иметь значение CLIP_PLANE0+i /// (i=0 - первая плоскость и т.д.) /// </param> /// <param name="equation"> /// Параметры уравнения плоскости: /// массив из 4-ех величин a, b, c, d уравнения плоскости ax+by+cz+d = 0 в объектной системе. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glClipPlane")] public static extern void ClipPlane(int plane, double[] equation); Здесь символьная константа /// <summary> /// Параметр метода ClipPlane, определяющий номер первой плоскости отсечения /// </summary> public const int CLIP_PLANE0 = 0x3000; Определять максимальное число плоскостей отсечения следует методом Get в форме (int)gl.Get(gl.MAX_CLIP_PLANES, 1)[0] Здесь константа MAX_CLIP_PLANES имеет значение /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// максимального числа плоскостей отсечения. 96 /// </summary> public const int MAX_CLIP_PLANES = 0x0D32; Для определения текущего уравнения плоскости отсечения следует использовать метод GetClipPlane /// <summary> /// Возвращает текущие уравнения заданной плоскости отсечения /// </summary> /// <param name="plane"> /// Номер плоскости отсечения в виде символьной константы CLIP_PLANE0+i /// </param> /// <param name="equation"> /// Массив коэффициентов уравнения плоскости. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glGetClipPlane")] public static extern void GetClipPlane(int plane, double[] equation); Для активации метода ClipPlane необходимо вызвать метод Enable с параметром CLIP_PLANE0+i для соответствующей плоскости i. Текущая активность метода ClipPlane для i-ой плоскости определяется методами Get или IsEnable с тем же параметром. Читатель, самостоятельно готовящий приложение, может внести в него описанные методы и проверить их эффект на отдельных примерах. В авторском приложении можно активировать любую плоскость отсечения, задавая для нее произвольное уравнение. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 11. Какой метод позволяет отсекать изображение плоскостями? 12. Каковы параметры метода отсечения плоскостями? 13. Как определить максимальное число отсекающих плоскостей, которое допускает система? 14. Как определить текущие уравнения отсекающих плоскостей? 15. Какой метод активирует процесс отсечения плоскостью? 4.3 Трафарет. Методы StencilFunc и StencilOp Трафарет используется для специальных эффектов, обеспечивающих воспроизведение различных комбинаций объектов. Например, один объект может играть роль трафарета – некоторую маску произвольной формы, в которой есть открытые и закрытые для воспроизведения области. Второй объект заполняет непрерывно область экрана. При воспроизведении непрерывного объекта можно сформулировать условия, при выполнении которых в буфер цвета будет попадать лишь та часть непрерывного объекта, которая совпадает с открытыми, или, наоборот, закрытыми областями трафарета. Для того чтобы отличать открытые области от закрытых областей, разным фрагментам объектатрафарета присваиваются различные числа – индексы, которые сохраняются в буфере трафарета. Пусть, скажем, у открытой области некоторого объекта-трафарета индекс равен 1, а у закрытой нулю. Часть пикселей непрерывного объекта имеет, таким образом, индекс 0 (попадает на закрытую область трафарета), а та часть, которая попадает на открытую область, имеет индекс единица. Если задать условием тестирования равенство индекса трафарета нулю, то в буфер цвета попадет только та часть непрерывного объекта, которая совпадает с закрытой областью объекта-трафарета (негатив). Если требовать равество индекса единице, то воспроизведется область непрерывного объекта, попадающая в открытую область объекта-трафарета (позитив). Активирует тест трафарета методом Enable с символьной константой /// <summary> 97 /// параметр активации тестирования по трафарету /// </summary> public const int STENCIL_TEST = 0x0B90; Тот же параметр используется методами IsEnable и Get для определения текущей активности теста трафарета. Операция сравнения и контрольное значение индекса задается командой StencilFunc, действующей подобно командам AlphaFunc и DepthFunc. /// <summary> /// Устанавливает функцию теста трафарета и индекс трафарета входящего пикселя. /// </summary> /// <param name="func"> /// Определяет функцию тестирования. Доступны 8 символьных констант: /// NEVER, LESS, LEQUAL, GREATER, GEQUAL, EQUAL, NOTEQUAL и ALWAYS. /// </param> /// <param name="refvalue"> /// Определяет индекс входящего пикселя в интервале [0; 2n - 1], /// где n - число битовых плоскостей в буфере трафарета. /// </param> /// <param name="mask"> /// Маска, на которую перед сравнением логически умножаются /// индекс входящего пикселя refvalue /// и индекс пикселя, находящегося в этот момент в буфере трафарета dest. /// Например, если первый параметр GREATER, /// то формула теста имеет вид refvalue & mask > dest & mask. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glStencilFunc")] public static extern void StencilFunc(int func, int refvalue, uint mask); Для определения текущих значений параметров метода StencilFunc следует использовать метод Get со следующими параметрами /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// символьной константы, определяющей текущий смысл /// функции тестирования трафарета в методе StencilFunc. /// </summary> public const int STENCIL_FUNC = 0x0B92; /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущего значения индекса пикселя в буфере трафарета, используемого методом /// StencilFunc. /// </summary> public const int STENCIL_REF = 0x0B97; /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущего значения маски, используемой методом StencilFunc. /// </summary> public const int STENCIL_VALUE_MASK = 0x0B93; По умолчанию значение func=ALWAYS, refvalue = 0 и mask = 255. Очистка буфера трафарета проводится методом Clear с параметром в виде символьной константы 98 /// <summary> /// Идентифицирует буфер трафарета в методах Clear и PushAttrib. /// </summary> public const int STENCIL_BUFFER_BIT = 0x00000400; Та же постоянная используется методом PushAttrib при сохранении состояния буфера трафарета в стеке. Метод ClearStencil устанавливает индекс, которым заполняется буфер трафарета при очистке /// <summary> /// Устанавливает индекс, которым заполняется буфер трафарета при его очистке. /// По умолчанию этот индекс равен нулю. /// </summary> /// <param name="s"> /// Значение индекса. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glClearStencil")] public static extern void ClearStencil(int s); Для определения значения очищающего индекса следует применить метод Get в форме (int)gl.Get(gl. STENCIL_CLEAR_VALUE, 1)[0] Здесь использована символьная константа /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// значения числа, которым заполняются битовые плоскости /// буфера трафарета при его очистке. /// </summary> public const int STENCIL_CLEAR_VALUE = 0x0B91; Метод StencilOP определяет действия, которые следует совершить после прохождения теста трафарета с индексами трафарета /// <summary> /// Устанавливает действия в результате прохождения пикселями теста трафарета /// Параметры StencilOp являются символьными константами. /// Доступны шесть символьных постоянных KEEP, ZERO, REPLACE, INCR, DECR и INVERT. /// </summary> /// <param name="sFail"> /// Определяет действие, если тест трафарета не проходит. /// </param> /// <param name="zFail"> /// Определяет действие, если пиксель проходит тест трафарета, но не проходит тест глубины. /// </param> /// <param name="szPass"> /// Определяет действие, если оба теста проходят. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glStencilOp")] public static extern void StencilOp(int sFail, int zFail, int szPass); Символьные константы, являющиеся возможными аргументами метода StencilOp, имеют вид // Символьные константы - аргументы метода StencilOp, // указывающие на тип действия, которым должно завершиться тестирование /// <summary> /// Оставить текущее значение индекса. 99 /// </summary> public const int KEEP = 0x1E00; /// <summary> /// Заменить индекс значением, указанным методом StencilFunc. /// </summary> public const int REPLACE = 0x1E01; /// <summary> /// Инвертировать биты индекса. /// </summary> public const int INVERT = 0x150A; /// <summary> /// Увеличить значение индекса на единицу, если его значение не максимально. /// </summary> public const int INCR = 0x1E02; /// <summary> /// Уменьшить значение индекса на единицу, если он не равен нулю. /// </summary> public const int DECR = 0x1E03; Возможна так же использованная ранее константа ZERO для зануления индекса. По умолчанию все три аргумента метода StencilOp равны KEEP. Для определения текущих значений параметров метода StencilOp следует использовать метод Get со следующими параметрами /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущей символьной константы, определяющей действие в методе StencilOp, /// если тест трафарета не проходит. /// </summary> public const int STENCIL_FAIL = 0x0B94; /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущей символьной константы, определяющей действие в методе StencilOp, /// если тест трафарета проходит, но не проходит тест глубины. /// </summary> public const int STENCIL_PASS_DEPTH_FAIL = 0x0B95; /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// текущей символьной константы, определяющей действие в методе StencilOp, /// если тест трафарета и тест глубины проходят оба. /// </summary> public const int STENCIL_PASS_DEPTH_PASS = 0x0B96; Для примера можно написать метод setStencilImage, который засылает в трафарет пиксели двух типов с индексами 1 и 2. Пиксели с индексами 1 будут содержать изображение кардиоиды, а с пиксели с индексом 2 – надпись контурными символами /// <summary> /// Создает изображение кардиоиды и надпись в буфере трафарета. /// </summary> void setStencilImage() { // Комментарий в строке статуса показывает значение индекса, 100 // очищающего буфер трафарета // и значения параметров методов StencilOp и StencilFunc по умолчанию stLabel.Text = "Stencil Clear Value=" + ((int)gl.Get(gl.STENCIL_CLEAR_VALUE, 1)[0]).ToString() + " Stencil fail=" + ((int)gl.Get(gl.STENCIL_FAIL, 1)[0]).ToString("x") + " Stencil pass depth fail=" + ((int)gl.Get(gl.STENCIL_PASS_DEPTH_FAIL, 1)[0]).ToString("x") + " Stencil pass depth path=" + ((int)gl.Get(gl.STENCIL_PASS_DEPTH_PASS, 1)[0]).ToString("x") + " func=" + ((int)gl.Get(gl.STENCIL_FUNC, 1)[0]).ToString("x") + " ref=" + ((int)gl.Get(gl.STENCIL_REF, 1)[0]).ToString() + " mask=" + ((uint)gl.Get(gl.STENCIL_VALUE_MASK, 1)[0]).ToString(); // Очистка буфера трафарета - заполнение значениями, очищающими буфер трафарета. gl.Clear(gl.STENCIL_BUFFER_BIT); // Формирование трафарета // Установка правил сравнения индексов пикселей, входящих в буфер трафарета, // с индексами пикселей, уже находящихся в буфере. // В данном случае задается функция ALWAYS, поэтому значение маски не важно, // значение ref = 1 становится индексом всех пикселей, попавших далее в буфер. gl.StencilFunc(gl.ALWAYS, 1, 0); // В этом случае тест проходит всегда (ALWAYS), // поэтому первый параметр StencilOp произволен. // Второй и третий параметры установлены в REPLACE, что позволяет пикселям // вне зависимости от результатов теста глубины, оставаться в буфере трафарета. gl.StencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE); gl.Enable(gl.STENCIL_TEST); gl.PushMatrix(); gl.Scale(.3f, .3f, 1); // Изображается кардиоида listMaker.Cardioid(gl.COMPILE_AND_EXECUTE, cardioidVertices, ref cardioidList); gl.PopMatrix(); gl.ListBase(globalOutlineGlyphsBaseList); // Здесь индексы трафарета следующих далее пикселей надписи принимают значения 2. gl.StencilFunc(gl.ALWAYS, 2, 0); gl.PushMatrix(); gl.Translate(-.13f, .1f, .1f); gl.Scale(.1f, .1f, .1f); gl.PushAttrib(gl.POLYGON_BIT); // Пишется текст контурным шрифтом gl.CallLists(7, gl.UNSIGNED_BYTE, "Stencil"); gl.PopAttrib(); gl.PopMatrix(); } Метод setStencilImage следует вызывать один раз до начала изображения целевых объектов. В коде авторского приложения целевым объектом является сфера, и весь алгоритм использования буфера трафарета выглядит следующим образом if (!stencilImageReady) { // Заполнение буфера трафарета проводится только один раз setStencilImage(); stencilImageReady = true; } 101 // Активация тестирования по трафарету gl.Enable(gl.STENCIL_TEST); // Функция формирования изображения: // пропускать только пиксели с индексом, определенным curRef gl.StencilFunc(gl.EQUAL, curRef, 255); // Значения параметров StencilOp по умолчанию – нет изменений в буфере трафарета. listMaker.Sphere(gl.COMPILE_AND_EXECUTE, sphereSlices, sphereStacks, 1, ref constSphereList); Здесь флаг stencilImageReady первоначально равен false, что позволяет использовать метод setStencilImage при первом прогоне, но не при каждом последующем. Поле curRef является переменным и в авторском приложении его значение определяется интерфейсом пользователя. Требование теста, установленные методом StencilFunc, таковы, что Если значение curRef равно 1, то будут изображаться только те пиксели сферы, которые попали внутрь кардиоиды, но не внутрь надписи. Если curRef = 0, то, наоборот, только те пиксели сферы, которые не попали ни внутрь кардиоиды, ни внутрь надписи. Наконец, если curRef = 2, то будут изображаться только пиксели, попавшие внутрь надписи. В этом коде на стадии получения изображения никаких изменений в буфере трафарета не производится. Читатель, самостоятельно готовящий приложение, может внести в него описанные методы и проверить их эффект на отдельных примерах. В авторском приложении можно проверить работу описанного кода, меняя значение curRef из интерфейса. Код этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 16. Как очищается буфер трафарета? 17. Как задается индекс очистки буфера трафарета? 18. Какое значение имеет индекс очистки буфера трафарета по умолчанию? 19. Как определяется текущее значение индекса очистки буфера трафарета? 20. Какой метод определяет действия с индексами пикселей в буфере трафарета в процессе тестирования трафарета? 21. Какие возможные значения имеют параметры метода StencilOp? 22. Как определить текущие значения параметров метода StencilOp? 23. Какие значения имеют параметры метода StencilOp по умолчанию? 24. Какой метод задает функцию тестирования? 25. Каким методом определить текущие значения параметров метода, управляющего функцией тестирования трафарета? 26. Какие значения имеют параметры метода, управляющего функцией тестирования, по умолчанию? 27. Какой параметр следует использовать в методе PushAttrib для сохранения в стеке состояния параметров всех методов, работающих с буфером трафарета? 28. Какой метод активирует тестирование трафарета? Примечание В OpenGL существует метод StencilMask с параметром mask, позволяющий маскировать запись в буфер трафарета произвольного набора битов. Фактически метод может блокировать отдельные битовые плоскости. По умолчанию все плоскости буфера трафарета открыты для записи. 102 4.4 Эффекты буфера-аккумулятора. Метод Accum Кроме уже знакомых нам буферов цвета, глубины и трафарета существует так называемый буфераккумулятор и методы OpenGL, которые позволяют его использовать. Буфер-аккумулятор служит для обмена информацией с буфером цвета в целом, а не с каждым пикселем в отдельности. Метод, регулирующий работу с буфером-аккумулятором Accum, позволяет загрузить все содержимое буфера цвета, произвести отдельные действия со всеми пикселями сразу, вернуть содержимое назад в буфер цвета. Для очистки буфера-аккумулятора используется метод Clear с параметром /// <summary> /// Идентифицирует буфер-аккумулятор в методах Clear и PushAttrib. /// </summary> public const int ACCUM_BUFFER_BIT = 0x00000200; Значение цвета, которым очищается буфер-аккумулятор задается методом /// <summary> /// Задает цвет очистки буфера-аккумулятора /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glClearAccum")] public static extern void ClearAccum(float red, float green, float blue, float alpha); Для определения текущего значения цвета очистки буфера-аккумулятора следует использовать метод Get с параметром /// <summary> /// Аргумент Get требует возврата значений 4 параметров /// цвета очистки буфера-аккумулятора. /// </summary> public const int ACCUM_CLEAR_VALUE = 0x0B80; Для сохранения текущего значения цвета очистки буфера-аккумулятора в стеке следует использовать метод PushAttrib с параметром ACCUM_BUFFER_BIT. Основной метод для работы с буфером-аккумулятором /// <summary> /// Оперирует с содержимым буфера аккумулятора. /// </summary> /// <param name="op"> /// Символьная константа указывает тип операции. /// Доступны значения ACCUM, LOAD, ADD, MULT и RETURN. /// </param> /// <param name="value"> /// Величина, участвующая в операциях в буфере аккумулятора. /// Смысл value зависит от типа операции – константы op. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glAccum")] public static extern void Accum(int op, float value); Символьные константы, определяющие тип операции с буфером-аккумулятором, имеют значения /// <summary> /// Цвета всех пикселей из буфера цвета умножаются на второй аргумент value метода Accum /// и добавляет к содержимому аккумулятора /// </summary> public const int ACCUM = 0x0100; /// <summary> 103 /// Цвета всех пикселей из буфера цвета умножаются на второй аргумент value метода Accum /// и загружаются в буфер-аккумулятор /// </summary> public const int LOAD = 0x0101; /// <summary> /// Цвета всех пикселей из буфера-аккумулятора умножаются на второй аргумент value метода /// Accum и загружаются в буфер цвета /// </summary> public const int RETURN = 0x0102; /// <summary> /// Цвета всех пикселей в буфере-аккумуляторе просто умножаются /// на второй аргумент value метода Accum /// </summary> public const int MULT = 0x0103; /// <summary> /// Цвета всех пикселей в буфере-аккумуляторе складываются /// со вторым аргументом value метода Accum. /// </summary> public const int ADD = 0x0104; Для иллюстрации работы метода Accum можно использовать код, написанный в авторском приложении. Этот код позволяет воспроизводить движение сферы по окружности с созданием эффекта затухающего следа. // Цвет фона в буфере цвета устанавливается черным gl.ClearColor(0, 0, 0, 1); // Очищается буфер цвета gl.Clear(gl.COLOR_BUFFER_BIT); // Инициализируется значение угла трансляции при выключенном таймере if (!timerEnabled) angleTranslate = 0; // Изображается сфера в текущем положении на окружности в плоскости XY gl.PushMatrix(); gl.LoadIdentity(); gl.Translate(.8f * (float)Math.Cos(angleTranslate * Math.PI / 180), .8f * (float)Math.Sin(angleTranslate * Math.PI / 180), 0); gl.Scale(.02f, .02f, .02f); listMaker.Sphere(gl.COMPILE_AND_EXECUTE, sphereSlices, sphereStacks, 1, ref constSphereList); gl.PopMatrix(); // В состоянии движения (при включенном таймере) if (timerEnabled) { // Интенсивность цветов пикселей в буфере-аккумуляторе уменьшается на 5% gl.Accum(gl.MULT, .95f); // К содержимому буфера-аккумулятора добавляется содержимое // буфера цвета без искажения цветов пикселей gl.Accum(gl.ACCUM, 1); // Содержимое буфера-аккумулятора переносится без искажений в буфер цвета gl.Accum(gl.RETURN, 1); } 104 else // При выключенном таймере (перед началом движения) { // Очищается буфер аккумулятора gl.Clear(gl.ACCUM_BUFFER_BIT); // Буфер цвета копируется в аккумулятор без искажений цветов пикселей gl.Accum(gl.LOAD, 1); // Включается таймер translateEnabled = timerEnabled = true; } Читатель, самостоятельно готовящий приложение, может внести в него приведенный код, или написать свой, иллюстрирующий работу метода Accum. В авторском приложении можно проверить работу описанного кода. Комментарий к коду этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 29. Какой метод очищает буфер-аккумулятор? 30. Какой метод задает цвет очистки буфера-аккумулятора? 31. Как определить текущий цвет очистки буфера-аккумулятора? 32. С каким параметром следует обратиться к методу PushAttrib, чтобы сохранить текущее значение цвета очистки буфера-аккумулятора в стеке? 33. Какие операции может осуществлять метод Accum? 4.5 Перспективная проекция Методы Frustum и Perspective До сих пор в изображении использовалось только одно преобразование от координат наблюдения к координатам отсечения – ортографическая проекция. Матрица ортографической проекции определяется методом Ortho, который вызывается в методе InitProjection формы f3D. Однако довольно часто используется другая, перспективная проекция, при которой удаляющийся объект уменьшает свои видимые размеры. В перспективной проекции наблюдатель видит объект, находясь в большом основании усеченного конуса (frustum) как у входа в тоннель, дальний вход в который соответствует малому основанию конуса. Координаты отсечения отсекают видимость наблюдателя стенками тоннеля, и его входами. Перед ближним входом и за дальним входом объект не виден. При перемещении по тоннелю видимые размеры объекта зависят от его удаления от ближнего входа. Перспективная проекция задается матрицами преобразования от координат наблюдения к координатам отсечения с помощью любого из двух стандартных методов Frustum и Perspective. Так выглядят описания этих методов /// <summary> /// Задает матрицу перспективной проекции с параметрами отсекающих плоскостей /// left, right, bottom, top, -near и -far. Параметры near и far должны быть больше нуля. /// При этом, far > near. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glFrustum")] public static extern void Frustum(double left, double right, double bottom, double top, double near, double far); /// <summary> /// Задает матрицу перспективной проекции. /// </summary> 105 /// <param name="fovy"> /// Угол обзора в градусах в направлении оси y (field of view angle). /// </param> /// <param name="aspect"> /// Отношение ширины порта наблюдения к его высоте. /// </param> /// <param name="near"> /// Положение ближней отсекающей плоскости с минусом. /// Параметр обязательно положителен. /// </param> /// <param name="far"> /// Положение дальней отсекающей плоскости с минусом. /// Параметр обязательно положителен и больше near /// </param> [DllImport("GLU32.DLL", EntryPoint = "gluPerspective")] public static extern void Perspective(double fovy, double aspect, double near, double far); Матрица преобразования, отвечающая команде Frustum, имеет вид Здесь l – left, r – right, t – top, b – bottom, n – near, f – far. Для примера, иллюстрирующего сравнительные характеристики изображения одного и того же объекта с разными проекционными матрицами, можно использовать приведенный ниже авторский код. В этом коде сфера непрерывно колеблется от ближнего до дальнего конца туннеля и обратно при регулируемых параметрах выбора самой матрицы и выбора координат отсечения по глубине float z; if (!timerEnabled) angleTranslate = 0; // Установка режима ввода матриц преобразования от координат наблюдения // к координатам отсечения gl.MatrixMode(gl.PROJECTION); // Ввод единичной матрицы gl.LoadIdentity(); // Выбор типа проекционной матрицы switch (curMatrix) { case 0: //ortho // Ввод матрицы ортогонального преобразования gl.Ortho(prjLeft, prjRight, prjBottom, prjTop, -(curNear = prjNear), -(curFar = prjFar)); break; case 1: // Ввод матрицы перспективной проекции frustum gl.Frustum(prjLeft, prjRight, prjBottom, prjTop,-curNear, -curFar); break; case 2: //Perspective gl.Perspective(curfovAngle, vpWidth / vpHeight,-curNear, -curFar); 106 break; } // Установка режима ввода матриц преобразования от объектных координат // к координатам наблюдения gl.MatrixMode(gl.MODELVIEW); gl.LoadIdentity(); // Изображается приближающаяся и удаляющаяся по оси z сфера // Координата z колеблется между значениями near и far gl.Translate(0, 0, z = .5f * (curNear - curFar) * (float)Math.Cos(angleTranslate * Math.PI / 180) + .5f * (curNear + curFar)); listMaker.Sphere(gl.COMPILE_AND_EXECUTE, sphereSlices, sphereStacks, 1, ref constSphereList); // тип преобразования и z-координата сферы выводятся в строку статуса stLabel.Text = (curMatrix == 0 ? "ortho " :curMatrix == 1 ? "frustum " : " perspective ") + z.ToString("f"); translateEnabled = timerEnabled = true; Здесь поля с префиксом cur могут регулироваться либо с пользовательского интерфейса (как это делается в авторском приложении), либо иным способом. Тест рубежного контроля 34. Какие методы обеспечивают преобразование к перспективной проекции? 35. Объясните смысл параметров методов преобразования к перспективной проекции. Туман. Метод Fog. В этом разделе рассматривается эффект изображения объекта, который называется "туманом" (fog). Никакого отношения к перспективной проекции эффект тумана и методы, которые его обеспечивают, не имеет. В то же время, внешний вид объекта в тумане зависит от расстояния до объекта от нуля системы координат наблюдения. Поэтому иллюстрация работы метода Fog приводится в настоящем разделе. Эффект тумана обеспечивается эффектом смешивания цвета тумана и цвета объекта. По умолчанию альфа-компонента цвета тумана нулевая. Факторы смешивания зависят от режима тумана. Режим определяется методом Fog. Здесь будут рассматриваться две версии метода Fog со следующими заголовками /// <summary> /// Определяет параметры тумана. /// </summary> /// <param name="pname"> /// Определяет тип параметра тумана со значением типа float. /// возможны значения FOG_MODE, FOG_DENSITY, FOG_START и FOG_END. /// </param> /// <param name="param"> /// Определяет числовое значение указанного параметра pname. /// Если pname=FOG_MODE, то значения должны быть из числа LINEAR, EXP и EXP2. /// По умолчанию EXP. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glFogf")] public static extern void Fog(int pname, float param); /// <summary> /// Определяет параметры тумана. 107 /// </summary> /// <param name="pname"> /// Определяет тип параметра тумана со значением типа float[]. /// Возможны значения FOG_MODE, FOG_DENSITY, FOG_START, FOG_END и FOG_COLOR. /// </param> /// <param name="fogparams"> /// Численное значение параметра pname. /// Если pname=FOG_MODE, то значения должны быть из числа LINEAR, EXP и EXP2. /// По умолчанию EXP. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glFogfv")] public static extern void Fog(int pname, float[] fogparams); Символьные константы-параметры метода Fog имеют вид /// <summary> /// Плотность тумана. Используется только в экспоненциальных режимах. По умолчанию 1. /// </summary> public const int FOG_DENSITY = 0x0B62; /// <summary> /// Ближайшее расстояние от наблюдателя, с которого начинается туман. /// Используется только в режиме линейного изменения. По умолчанию 0. /// </summary> public const int FOG_START = 0x0B63; /// <summary> /// Дальнее расстояние от наблюдателя, где туман обрывается. /// Используется только в режиме линейного изменения. По умолчанию 1. /// </summary> public const int FOG_END = 0x0B64; /// <summary> /// Тип режима. /// </summary> public const int FOG_MODE = 0x0B65; /// <summary> /// Цвет тумана. Массив из 4-ех значений. По умолчанию 0,0,0,0. /// </summary> public const int FOG_COLOR = 0x0B66; /// <summary> /// Линейный тип тумана. Фактор смешивания f своего цвета с цветом объекта определяется /// формулой f = (e - c)/(e - s). /// Здесь c - расстояние от начала координат наблюдения /// до центра изображаемого фрагмента, /// s-значение параметра FOG_START (по умолчанию 0), /// а e-значение параметра FOG_END (по умолчанию 1). /// </summary> public const int LINEAR = 0x2601; /// <summary> /// Экспоненциальный тип тумана. Фактор смешивания f меняется по закону f = Exp(-d*c). /// Здесь c - расстояние от начала координат наблюдения /// до центра изображаемого фрагмента, d - значение параметра FOG_DENSITY /// </summary> 108 public const int EXP = 0x0800; /// <summary> /// Экспоненциальный тип тумана. Фактор смешивания f меняется по закону f = Exp(-(d*c)^2). /// Здесь c - расстояние от начала координат наблюдения до центра изображаемого /// фрагмента, d - значение параметра FOG_DENSITY /// </summary> public const int EXP2 = 0x0801; Для определения текущих значений параметров метода Fog следует использовать метод Get с уже перечисленными параметрами в форме gl.Get(gl.FOG_COLOR, 4)[i], где i = 0,1,2,3 – цвета красный, зеленый, синий и альфа-компонента gl.Get(gl.FOG_DENSITY, 1)[0] (int)gl.Get(gl.FOG_MODE, 1)[0] gl.Get(gl.FOG_START, 1)[0] gl.Get(gl.FOG_END, 1)[0] Для сохранения параметров тумана в стеке надо использовать метод PushAttrib с параметром /// <summary> /// Параметр метода PushAttrib /// </summary> public const int FOG_BIT = 0x00000080; Для активации тумана используется метод Enable с параметром /// <summary> /// Параметр методов Enable, Get, IsEnabled, Disable. /// </summary> public const int FOG = 0x0B60; Для определения текущей активности тумана – метод IsEnabled или Get с тем же параметром. Читатель, самостоятельно готовящий приложение, может использовать в нем приведенные методы по регулировке и активизации тумана. Авторское приложение позволяет наблюдать и регулировать эффект тумана параллельно с эффектами различных проекций. Комментарий к коду этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 36. Какой метод регулирует эффект тумана? 37. Опишите параметры метода, регулирующего эффект тумана. 38. Как определить текущие значения параметров метода, регулирующего эффект тумана? 39. Какой параметр необходимо использовать в методе PushAttrib, чтобы сохранить в стеке параметры метода Fog? 40. Какой метод активирует эффект тумана? 4.6 Scissor Test Существует возможность выделять (вырезать – scissor) прямоугольник изображения в оконных координатах, отсекая от попадания в буфер фрейма той части объекта, которая лежит вне границ прямоугольника. Это похоже на действие ClipPlane. Разница в том, что уравнения ClipPlane задаются в объектных координатах, тогда как в данном случае отсечение происходит в оконных координатах. Управляет этим отсечением метод Scissor с заголовком /// <summary> /// Отсекает в оконных координатах прямоугольник с заданными параметрами. /// Та часть изображения, которая лежит вне указанного прямоугольника /// не попадает в буфер фрейма. 109 /// </summary> /// <param name="x"> /// Оконная координата левой границы. /// </param> /// <param name="y"> /// Оконная координата нижней границы. /// </param> /// <param name="width"> /// Ширина в пикселях. /// </param> /// <param name="height"> /// Высота в пикселях. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glScissor")] public static extern void Scissor(int x, int y, int width, int height); Для определения текущих значений параметров метода Scissor следует использовать метод Get в следующей форме (int)gl.Get(gl.SCISSOR_BOX, 4)[i], где i = 0,1,2,3 – возвращает соответственно x, y, width и height параметры метода Scissor. По умолчанию x=0, y=0, а ширина и высота определяется размерами окна вывода. Символьная константа SCISSOR_BOX имеет вид /// <summary> /// Аргумент Get требует возврата значений 4 параметров метода Scissor (x,y,width и height). /// </summary> public const int SCISSOR_BOX = 0x0C10; Для сохранения в стеке текущего состояния метода Scissor следует использовать метод PushAttrib с параметром /// <summary> /// Маска атрибутов метода Scissor /// </summary> public const int SCISSOR_BIT = 0x00080000; Для активации метода Scissor и, собственно, самого теста необходим вызов метода Enable с параметром /// <summary> /// Параметр активации метода Scissor. Используется методами Get, Enable, IsEnabled и Disable. /// </summary> public const int SCISSOR_TEST = 0x0C11; Тот же параметр следует применить в методе IsEnabled или Get для определения текущего состояния активности метода Scissor. Читатель, самостоятельно готовящий приложение, может использовать в нем приведенные методы по регулировке и активизации scissor-теста. Авторское приложение позволяет наблюдать и регулировать эффект scissor-теста с помощью интерфейсных элементов управления. Комментарий к коду этой части авторского приложения можно найти по ссылке. Тест рубежного контроля 41. Какой метод позволяет отсекать изображение в оконных координатах? 42. Опишите параметры метода отсечения в оконных кординатах. 43. Как определить текущие значения параметров метода отсечения? 110 44. Как сохранить в стеке текущее состояние и параметры scissor-теста? 45. Как активировать тест отсечения? Remaining В этом разделе по мере возможности будут изучаться оставшиеся не рассмотренными методы библиотеки OpenGL. Важный класс методов относится к категории "Освещение" (lighting). Методы этой категории позволяют создать вполне реалистичное изображение объектов путем использования специальных формул расчета цветов пикселей, учитывающих освещение. Модуль 5. Освещение (lighting) Комплексной целью модуля является изучение следующих элементов OpenGL В создании эффекта освещения принимают участие несколько методов. Один из них метод LightModel описывает режим освещения Метод Material описывает реакцию объекта на освещение. Метод ColorMaterial комбинирует заданные цвета вершин объекта с эффектом освещения. Метод Light описывает свойства источников света 5.0 Среда и вещество Освещение сцены и два дополнительных параметра освещения регулируются методом LightModel. Метод LightModel Заголовок этого метода имеет вид /// <summary> /// Устанавливает цвет сцены и параметры модели освещения. /// </summary> /// <param name="pname"> /// Определяет тип вводимых параметров. /// Допустимы значения: /// LIGHT_MODEL_AMBIENT - цвет освещенности сцены, или "окружения" /// (по умолчанию (0.2, 0.2, 0.2, 1)), /// LIGHT_MODEL_LOCAL_VIEWER - способ расчета угла зеркального отражения. /// Значение 0 (по умолчанию) предполагает положение неподвижного наблюдателя /// в начале системы координат наблюдения; /// значение 1 предполагает, что наблюдатель смотрит в направлении, обратном оси z /// объектной системы координат (локальный, или подвижный наблюдатель); /// LIGHT_MODEL_TWO_SIDE – в расчет освещения принимаются цвета /// отдельно для каждой поверхности (значение 1), /// либо только одной – передней грани (значение 0). По умолчанию 0. /// </param> /// <param name="fparams"> /// Массив значений параметров. /// В случае LIGHT_MODEL_AMBIENT массив должен содержать 4 числа - цвет окружения, /// в остальных случаях - по одному числу 0 или 1. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glLightModelfv")] public static extern void LightModel(int pname, float[] fparams); Символьные константы, используемые как параметры метода LightModel, имеют значения public const int LIGHT_MODEL_LOCAL_VIEWER = 0x0B51; 111 public const int LIGHT_MODEL_TWO_SIDE = 0x0B52; public const int LIGHT_MODEL_AMBIENT = 0x0B53; Для определения текущих значений параметров метода LightModel следует использовать метод Get с теми же константами в качестве параметров gl.Get(gl.LIGHT_MODEL_AMBIENT, 4)[i] - цвет сцены, i = 0,1,2,3 gl.Get(gl.LIGHT_MODEL_TWO_SIDE, 1)[0] gl.Get(gl.LIGHT_MODEL_LOCAL_VIEWER, 1)[0] Для сохранения в стеке параметров освещения вообще и параметров метода LightModel в частности следует использовать метод PushAttrib с параметром /// <summary> /// Маска битов параметров освещения. /// </summary> public const int LIGHTING_BIT = 0x00000040; Для активации метода LightModel в частности и освещения вообще следует использовать метод Enable с параметром /// <summary> /// Параметр активации освещения для методов Enable, IsEnabled, Disable, Get /// </summary> public const int LIGHTING = 0x0B50; Тот же параметр используется методами Get и IsEnabled для определения состояния активации освещения. Тест рубежного контроля 1. Опишите смысл параметров метода LightModel. 2. Какой метод возвращает текущие значения параметров метода LightModel? 3. Каков цвет среды по умолчанию? 4. Какой параметр следует использовать в методе PushAttrib с тем, чтобы сохранить состояние освещенности в стеке? 5. Каким методом активируется освещенность? Метод Material Метод Material определяет индивидуальные свойства реакции на освещенность материала изображаемого объекта. С его помощью можно определить цвет излучения обеих граней объекта фронтальной и задней, цвета отражения любыми гранями окружающей составляющей света, диффузной составляющей и зеркальной составляющей. Наконец, метод Material определяет коэффициент отражения зеркальной составляющей света – так называемую яркость. Так выглядит заголовок метода Material /// <summary> /// Определяет параметры вещества объекта в режиме освещения. /// </summary> /// <param name="face"> /// Определяет грань многоугольника. /// Допускает три значения FRONT, BACK или FRONT_AND_BACK. /// </param> /// <param name="name"> /// Определяет тип параметров. Допускает значения: /// AMBIENT- цвет отраженного света окружения (по умолчанию (0.2, 0.2, 0.2, 1)), /// DIFFUSE - цвет диффузно отраженного света (по умолчанию (0.8, 0.8, 0.8, 1)), /// SPECULAR - цвет зеркально отраженного света (по умолчанию (0, 0, 0, 1)), 112 /// EMISSION - цвет излученного света (по умолчанию (0, 0, 0, 1)), /// SHININESS - коэффициент яркости в зеркальной составляющей; /// значения SHININESS лежат в интервале [0; 128] (по умолчанию 0), /// AMBIENT_AND_DIFFUSE - если цвета диффузно отраженного света /// и света окружения совпадают. /// </param> /// <param name="parameter"> /// Массив параметров. Размерность массива 1 в случае SHININESS. В остальных случаях - 4. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glMaterialfv")] public static extern void Material(int face, int name, float[] parameter); Константы параметра name имеют вид public const int AMBIENT = 0x1200; public const int DIFFUSE = 0x1201; public const int SPECULAR = 0x1202; public const int EMISSION = 0x1600; public const int SHININESS = 0x1601; public const int AMBIENT_AND_DIFFUSE = 0x1602; Для определения текущих значений параметров метода Material следует использовать метод GetMaterial /// <summary> /// Определяет текущие значения параметров метода Material. /// </summary> /// <param name="face"> /// Тип грани FRONT или BACK /// </param> /// <param name="pname"> /// имя параметра AMBIENT, DIFFUSE, SPECULAR, EMISSION или SHININESS /// </param> /// <param name="fparams"> /// Массив, в котором возвращается значение параметра запрашиваемого типа. /// В случае цветов размер массива равен 4, а в случае SHININESS единице. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glGetMaterialfv")] public static extern void GetMaterial(int face, int pname, float[] fparams); Метод Material активируется методом Enable с параметром LIGHTING. Если ни один источник света не включен, и если отключена активность метода ColorMaterial (см. ниже), то цвет объекта при включенном освещении определяется как сумма цвета, излучаемого веществом ecm, и скалярного произведения цвета среды acs на цвет отраженного материалом света окружения acm. В частности, по умолчанию цвет всех вершин равен ecm + acsacm = (0, 0, 0, 1) + (0.2, 0.2, 0.2, 1)*(0.2, 0.2, 0.2, 1) = (0, 0, 0, 1) + (0.04, 0.04, 0.04, 1) = (0.04, 0.04, 0.04, 1). Диффузная составляющая, равно как и зеркальная составляющая объекта включаются в формулу цвета вершин объекта только при включенном источнике света. Тест рубежного контроля 6. Опишите смысл параметров метода Material. 7. Какой метод определяет текущие значения параметров метода Material? 8. Какова интенсивность отражаемого света среды по умолчанию? 9. Какова интенсивность излучаемого света по умолчанию? 113 10. Каково значение цвета диффузно отражаемого света по умолчанию? 11. Каким методом активируется метод Material? 12. Можно ли придавать различные интенсивности, например, отражаемого света окружения, различным граням многоугольника? 13. При каком условии будет проявлять себя интенсивность отражаемого задней стороной многоугольника света окружения? Метод ColorMaterial Метод ColorMaterial при включенном освещении формирует цвета, указанные методом Color для каждой вершины отдельно. Его параметры определяют освещаемые грани и смысл цвета (излучение, отражение света сцены, диффузное отражение или зеркальное отражение), указанного методом Color. Вот заголовок метода ColorMaterial /// <summary> /// Использует цвета вершин в качестве цветов вещества при включенном освещении. /// </summary> /// <param name="face"> /// Тип грани многоугольника. Допустимы значения FRONT, BACK или FRONT_AND_BACK. /// По умолчанию FRONT_AND_BACK. /// </param> /// <param name="mode"> /// Тип цвета, которому отвечают цвета вершин. /// Допустимы значения AMBIENT, DIFFUSE, SPECULAR, EMISSION и AMBIENT_AND_DIFFUSE. /// По умолчанию AMBIENT_AND_DIFFUSE. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glColorMaterial")] public static extern void ColorMaterial(int face, int mode); Определяет текущие значения параметров метода ColorMaterial метод Get в форме (int)gl.Get(gl.COLOR_MATERIAL_FACE, 1)[0] (int)gl.Get(gl.COLOR_MATERIAL_PARAMETER, 1)[0] Константы-параметры метода Get имеют значения /// <summary> /// Аргумент Get требует возврата значения 1 параметра /// символьной постоянной, определяющей грань многоугольника, /// установленную методом ColorMaterial. /// </summary> public const int COLOR_MATERIAL_FACE = 0x0B55; /// <summary> /// Аргумент Get требует возврата значения 1 параметра - символьной постоянной, /// определяющей тип цвета, установленный методом ColorMaterial. /// </summary> public const int COLOR_MATERIAL_PARAMETER = 0x0B56; Метод ColorMaterial должен быть активирован методом Enable с параметром /// <summary> /// Параметр активации метода ColorMaterial. /// Используется методами Get, Enable, IsEnabled и Disable. /// </summary> public const int COLOR_MATERIAL = 0x0B57; Определить состояние активности метода ColorMaterial можно методом Get или IsEnabled с тем же параметром. 114 Тест рубежного контроля 14. Опишите смысл параметров метода ColorMaterial. 15. Какой метод возвращает текущие значения параметров метода ColorMaterial? 16. Каковы значения параметров метода ColorMaterial по умолчанию? 17. Как активировать метод ColorMaterial? 18. Какие интенсивности интерпретируются в качестве цветов-параметров метода ColorMaterial? Метод ShadeModel Метод ShadeModel определяет режим затенения или обработку полутонов при изображении многоугольников в зависимости от двух значений его единственного параметра /// <summary> /// Определяет режим изображения многоугольников. /// При выключенных источниках света эффект не заметен. /// </summary> /// <param name="mode"> /// Режим изображения. Допускает две символьные постоянные SMOOTH и FLAT. /// По умолчанию SMOOTH. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glShadeModel")] public static extern void ShadeModel(int mode); Константы, используемые методом ShadeModel, имеют вид /// <summary> /// Параметр метода ShadeModel. /// Изображает все цвета пикселей, окружающих вершину, цветом вершины. /// </summary> public const int FLAT = 0x1D00; /// <summary> /// Параметр метода ShadeModel. /// Обеспечивает интерполяцию цветов вершин для плавного перехода от одной вершины к другой. /// </summary> public const int SMOOTH = 0x1D01; Текущее значение параметра метода ShadeModel определяется методом Get в форме (int)gl.Get(gl.SHADE_MODEL, 1)[0] Параметр SHADE_MODEL имеет значение /// <summary> /// Аргумент Get требует возврата значения 1 параметра - символьной постоянной, /// определяющей тип затенения, установленный методом ShadeModel. /// </summary> public const int SHADE_MODEL = 0x0B54; Примечание. При выключенных источниках света эффект переключения параметров метода ShadeModel себя не проявляет. Изображение остается таким же, как при выборе значения SMOOTH. Тест рубежного контроля 19. Для чего используется метод ShadeModel? 20. В чем смысл параметра метода ShadeModel. 21. Как определить текущее значение параметра метода ShadeModel? 115 Читатель, самостоятельно готовящий приложение, может использовать в нем приведенные методы LightModel, Material и ColorMaterial для наблюдения эффекта от включения освещения и изменений параметров этих методов на примере какого-либо объекта. Авторское приложение позволяет наблюдать и регулировать параметры приведенных методов с помощью интерфейсных элементов управления. Комментарий к коду этой части авторского приложения можно найти по ссылке. 5.1 Источники света Метод Light в OpenGL регулирует параметры источников света /// <summary> /// Устанавливает параметры источника света. /// </summary> /// <param name="light"> /// Номер источника света. Допустимы значения LIGHT0+i. /// Максимальное значение i-1 определяется функцией Get с параметром MAX_LIGHTS. /// </param> /// <param name="name"> /// Определяет параметр источника света. Допустимы значения: /// AMBIENT - компонента окружения источника (по умолчанию (0, 0, 0, 1)), /// DIFFUSE - компонента диффузного отражения источника /// (по умолчанию для источника 0 (1, 1, 1, 1), для других (0, 0, 0, 1)), /// SPECULAR - компонента зеркального отражения источника /// (по умолчанию для источника 0 (1, 1, 1, 1), для других (0, 0, 0, 1)), /// POSITION - положение источника света в объектных координатах /// (по умолчанию (0, 0, 1, 0)), /// SPOT_DIRECTION - направление луча источника (по умолчанию (0,0,-1)), /// SPOT_EXPONENT – степень рассеяния луча света (по умолчанию 0), /// SPOT_CUTOFF - угол раствора луча в градусах (по умолчанию 180), /// CONSTANT_ATTENUATION - положительная константа затухания (по умолчанию 1), /// LINEAR_ATTENUATION – положит. коэффициент линейного затухания (по умолчанию 0), /// QUADRATIC_ATTENUATION – положит. коэфф. квадратичного затухания (по умолчанию 0). /// </param> /// <param name="parameter"> /// Массив параметров. Размерность массива зависит от смысла параметра name. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glLightfv")] public static extern void Light(int light, int name, float[] parameter); Символьная константа параметра light имеет вид /// <summary> /// Параметр, нумерующий первый источник света. /// Остальные источники нумеруются как LIGHT0+i. /// </summary> public const int LIGHT0 = 0x4000; Константа максимального числа источников света /// <summary> /// Аргумент Get требует возврата значения 1 параметра - максимального /// числа источников света. 116 /// </summary> public const int MAX_LIGHTS = 0x0D31; Для активации источника света следует использовать метод Enable с параметром LIGHT0+i. Текущее состояние активации источника света определяется методом IsEnable или Get с тем же параметром. Константы параметра name метода Light, относящиеся к цветам, указаны в предыдущих разделах. Оставшиеся константы имеют вид public const int POSITION = 0x1203; public const int SPOT_DIRECTION = 0x1204; public const int SPOT_EXPONENT = 0x1205; public const int SPOT_CUTOFF = 0x1206; public const int CONSTANT_ATTENUATION = 0x1207; public const int LINEAR_ATTENUATION = 0x1208; public const int QUADRATIC_ATTENUATION = 0x1209; Для определения текущих значений параметров метода Light следует использовать метод /// <summary> /// Возвращает текущие значения параметров метода Light /// </summary> /// <param name="light"> /// Номер источника света в форме LIGHT0+i /// </param> /// <param name="pname"> /// Название параметра из числа AMBIENT, DIFFUSE, SPECULAR, POSITION, /// SPOT_DIRECTION, SPOT_EXPONENT, SPOT_CUTOFF, CONSTANT_ATTENUATION, /// LINEAR_ATTENUATION, QUADRATIC_ATTENUATION. /// </param> /// <param name="lparams"> /// Массив возвращаемого значения параметра. Размер массива зависит от типа параметра. /// </param> [DllImport("OPENGL32.DLL", EntryPoint = "glGetLightfv")] public static extern void GetLight(int light, int pname, float[] lparams); При использовании источников света изображение объекта, вообще говоря, зависит от нормали в каждой точке поверхности. Поэтому во всех списках объектов перед каждой вершиной (метод Vertex) следует вызвать метод Normal с тремя параметрами, которые указывают вектор (не обязательно единичный) внешней нормали к поверхности в данной точке. Так выглядит заголовок метода Normal /// <summary> /// Определяет вектор (nx, ny, nz) (не обязательно нормированный) направления /// нормали в точке положения вершины. /// </summary> [DllImport("OPENGL32.DLL", EntryPoint = "glNormal3f")] public static extern void Normal(float nx, float ny, float nz); Здесь дается редакция методов, создающих списки сферы, тора и усеченного конуса, описанных в классе listMaker библиотеки GL, с учетом нормализации вершин: Сфера (метод Sphere): o При формировании северного полюса код принимает вид gl.Normal(0, 1, 0); gl.Vertex(0, 1, 0); for (int slice = 0; slice <= slices; slice++) { gl.Normal(sinteta[1] * sinfi[slice], costeta[1], sinteta[1] * cosfi[slice]); 117 gl.Vertex(sinteta[1] * sinfi[slice], costeta[1], sinteta[1] * cosfi[slice]); } При формировании средней части сферы код принимает вид for (int slice = 0; slice <= slices; slice++) { gl.Normal(sinteta[stack] * sinfi[slice], costeta[stack], sinteta[stack] * cosfi[slice]); gl.Vertex(sinteta[stack] * sinfi[slice], costeta[stack], sinteta[stack] * cosfi[slice]); gl.Normal(sinteta[stack + 1] * sinfi[slice], costeta[stack + 1], sinteta[stack + 1] * cosfi[slice]); gl.Vertex(sinteta[stack + 1] * sinfi[slice], costeta[stack + 1], sinteta[stack + 1] * cosfi[slice]); } o При формировании южного полюса gl.Normal(0, -1, 0); gl.Vertex(0, -1, 0); for (int slice = slices; slice >= 0; slice--) { gl.Normal(sinteta[stacks - 1] * sinfi[slice], costeta[stacks - 1], sinteta[stacks - 1] * cosfi[slice]); gl.Vertex(sinteta[stacks - 1] * sinfi[slice], costeta[stacks - 1], sinteta[stacks - 1] * cosfi[slice]); } Тор (метод Torus): for (int bigSlice = bigSlices; bigSlice >= 0; bigSlice--) { gl.Normal(cosBigFi[bigSlice] * cosSmallFi[smallSlice], cosSmallFi[smallSlice] * sinBigFi[bigSlice], sinSmallFi[smallSlice]); gl.Vertex(curRad * cosBigFi[bigSlice], curRad * sinBigFi[bigSlice], smallRadius * sinSmallFi[smallSlice]); gl.Normal(cosBigFi[bigSlice] * cosSmallFi[smallSlice + 1], cosSmallFi[smallSlice + 1] * sinBigFi[bigSlice], sinSmallFi[smallSlice + 1]); gl.Vertex(nextRad * cosBigFi[bigSlice], nextRad * sinBigFi[bigSlice], smallRadius * sinSmallFi[smallSlice + 1]); } Усеченный конус (метод Frustum): o Добавляется локальная переменная float zNorm = (float)Math.Cos(Math.Atan2(1, 1 - topRadius)); o Цикл вершины конуса при нулевом радиусе принимает вид for (int slice = slices; slice >= 0; slice--) { x = curRadius * cosfi[slice]; y = curRadius * sinfi[slice]; gl.Normal(x, y, zNorm); gl.Vertex(x, y, z); } o Цикл основной поверхности конуса принимает вид for (int slice = slices; slice >= 0; slice--) o 118 { x = curRadius * cosfi[slice]; y = curRadius * sinfi[slice]; xNext = radiusNext * cosfi[slice]; yNext = radiusNext * sinfi[slice]; gl.Normal(x, y, zNorm); gl.Vertex(x, y, z); gl.Normal(xNext, yNext, zNorm); gl.Vertex(xNext, yNext, zNext); } Определение текущих значений параметров метода Normal осуществляется методом Get с параметром /// <summary> /// Аргумент Get требует возврата значения трех параметров – /// текущих значений аргументов метода Normal. /// </summary> public const int CURRENT_NORMAL = 0x0B02; Для эффективного применения метода Normal следует так же активировать режим нормализации методом Enable с параметром /// <summary> /// Параметр активации нормализации. /// Используется методами Get, Enable, IsEnabled и Disable. /// </summary> public const int NORMALIZE = 0x0BA1; Эта активация нормализует векторы нормали, если они не единичные. Для сохранения флага NORMALIZE в стеке следует использовать метод PushAttrib с параметром ENABLE_BIT. При включенных источниках света с учетом всех перечисленных выше параметров формула цвета каждой вершины представляет собой сумму интенсивности цвета излучения материи, произведения интенсивностей цветов отражения материей света среды и самого света среды и вклада от каждого активного источника света. Вклад источника света состоит из трех компонент – компоненты среды, диффузной компоненты и компоненты интенсивности зеркально отражаемого света. 1. Компонента среды является произведением интенсивностей цвета отраженного материей света среды и света среды, создаваемого данным источником. 2. Диффузная компонента является произведением интенсивностей цвета диффузного отражения материи, интенсивности цвета диффузной составляющей света источника и скалярного произведения нормали вершины с нормализованным вектором направления от вершины к источнику света. 3. Компонента зеркально отражаемого света является произведением интенсивности цвета, зеркально отраженного материей света, интенсивности цвета зеркально отражаемой составляющей источника света и скалярного произведения нормали вершины на нормализованный сумму векторов, направленных от вершины к наблюдателю и от вершины к источнику света, возведенного в степень яркости материи. Наблюдатель расположен по умолчанию в положительной бесконечности оси Z, а если параметр LIGHT_MODEL_LOCAL_VIEWER метода LightModel установлен равным единице, то наблюдатель находится в начале системы координат наблюдения. 119 Все три компоненты умножаются одинаково на множитель, уменьшающий интенсивность и состоящий из двух множителей: 1. Первый множитель имеет вид 1/ (K0 + K1d + K2d2), где K0, K1 и K2 – соответственно, постоянный коэффициент затухания CONSTANT_ATTENUATION , линейный LINEAR_ATTENUATION и квадратичный QUADRATIC_ATTENUATION, d – расстояние от источника света до вершины. Он равен единице, если источник света находится на бесконечности (w = 0), как по умолчанию. 2. Второй множитель зависит от направления луча света SPOT_DIRECTION, угла раствора пучка света SPOT_CUTOFF и степени рассеяния луча SPOT_EXPONENT. Он равен a. единице, если угол раствора SPOT_CUTOFF равен 1800, как по умолчанию, b. нулю, если скалярное произведение единичного вектора от источника света к вершине и единичного вектора направления луча SPOT_DIRECTION меньше косинуса угла раствора SPOT_CUTOFF, c. скалярному произведению единичных векторов от источника света к вершине и направления луча SPOT_DIRECTION, возведенному в степень рассеяния луча SPOT_EXPONENT в других случаях. Все скалярные произведения, которые оказываются отрицательными, заменяются нулями. Альфа-компонентой результирующего цвета является альфа-компонента диффузной составляющей материи. Читатель, самостоятельно готовящий приложение, может использовать в нем приведенные методы Light и Normal для наблюдения эффекта от включения источников света и изменения параметров этих источников на примере какого-либо объекта. Авторское приложение позволяет наблюдать и регулировать параметры приведенных методов с помощью интерфейсных элементов управления. Комментарий к коду этой части авторского приложения можно найти по ссылке. Проектные задания 1. Оформите освещение для часов, которые готовились в предыдущих проектных заданиях. 2. Постройте список изображения примитивной кубической ячейки кристалла с произвольным числом и размером частиц, находящихся в произвольном положении, подобную той, что изображена ниже 120 Тест рубежного контроля 22. Опишите параметры источников света и их значения по умолчанию. 23. Какой метод активирует источник света? 24. Какой метод возвращает текущие значения параметров источника света? 25. Что делает метод Normal? 26. Какой параметр следует использовать в методе PushAttrib с тем, чтобы сохранить состояние освещенности в стеке? 27. Опишите формулу цвета вершины при освещении. 28. Какое значение имеет альфа-компонента вершины при освещении?