Opengl работа с картинками


Содержание

6.1 Уроки OpenGL. Создание растрового редактора на OpenGL + C#.

1. Введение, постановка задач.

2. Изучение модели работы программы. Описание создаваемых классов и методов их взаимодействий.

Итак, начнем мы с определения того, что понимается под растровым рисунком.

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

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

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

Сейчас мы реализуем не все функции, а лишь самые главные, которые должны обеспечить работоспособность программы – основа оболочки, класс движка, класс слоев, класс кистей. Работать на самом деле будет только один слой, одна кисть и алгоритмы инициализации OpenGL (в оболочке), а также визуализации.
Большую часть мы усложним и доработаем в последствии. Схема работы программы так же представлена на рисунке 1.
Рисунок 1. Схема работы разрабатываемого растрового редактора.
Теперь когда наши цели определены и оглашена схема работы программы, мы перейдем к написанию её основы.

3. Создание базовой оболочки программы.

На окне будут расположены следующие элементы (описание в соответствии с цифрами на рисунке 2):

  1. Меню программы. На данный момент мы создадим 3 раздела меню: Файл, Рисование и Слои (их подменю можно увидеть на рисунке 3).
    Рисунок 3. Примеры создаваемых меню.
  2. Панель инструментов. Ширина панели 44 пикселя. Создайте на будущее заготовки 3-х кнопок. Эту панель в последующих частях главы мы заполним кнопками для установки режимов рисования кистей, рисования геометрических объектов и т.д.
  3. Элемент SimpleOpenGLControl. Расположите его так, как показано на рисунке, затем свойство name установите равным AnT. Здесь будет проходить основной render.
  4. Здесь находится элемент CheckedListBox. В следующей части главы мы реализуем систему слоев, которые будут активно участвовать в визуализации. В этом элементе будет отображаться список наших слоев. Оболочка также получит функции для редактирования параметров слоев.
  5. Здесь будет находиться панель инструментов. Пока что мы расположим на ней 2 кнопки. В дальнейшем кнопки этой панели будут отвечать за операции над слоями.

Если у вас возникли какие-либо вопросы в процессе создания элементов управления или настройки начальной инициализации элемента SimpleOpneGLControl, то напомним вам, что в главе 2.2 описывается процесс создания меню и панелей инструментов, а в главе 4.4 – процесс установки элементов для визуализации OpenGL и их первоначальной инициализации.

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

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

Далее добавим код объявления экземпляра класса движка растрового редактора:

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

Теперь добавьте один таймер, назовите его RenderTimer. Затем добавьте обработку события загрузки формы.
Функция, вызываемая событием загрузки формы (чтобы ее добавить, щелкните двойным щелчком левой клавиши мыши по заголовку окна или перейдите через свойства формы приложения к списку доступных событий (event) и добавьте двойным щелчком в пустой области обработку события Load).

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

Как видно из кода, мы создаем 2D ортогональную проекцию, причем в отличие от кода, который мы использовали в предыдущих главах, здесь мы устанавливаем проекцию таким образом, что размер видимой области в проекции будет равен размерам элемента AnT. Другими словами, координата X на элементе AnT будет равна координате X видимой части в координатной системе OpenGL. Но координатная ось Y направлена в противоположенную сторону, поэтому координата Y в координатной системе OpenGL будет равна AnT.HeightY’ (Y’ – координата Y на элементе AnT).

После настройки проекции мы инициализируем экземпляр движка растрового редактора и активируем таймер.
В настройках таймера добавьте обработчик события Tick, либо выделив элемент таймер (далее перейти к его свойствам – меню Event, затем двойной щелчок на пустой строке справа от события event), либо просто двойным щелчком на элементе RenderTimer.
В свойствах таймера установите интервал равным 30 миллисекундам.

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

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

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

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

Здесь немного коснемся того, что же происходит. С инициализацией OpenGL в функции Form1_Load, созданием счетчиков и назначением обработчиков событий – все понятно (иначе вы не выполняли предыдущие главы).

Функция обработки движения мыши проверяет, нажата ли левая клавиша мыши. Если да, то будет вызываться метод рисования кистью (Drawing), который мы далее реализуем в классе anEngine. В этот метод в качестве параметров передаются значения координаты на элементе AnTX и AnT.HeightY (так как оси Y у нас направлены в противоположенные стороны, мы это уже обсудили в настройках инициализации OpenGL).

Функция Drawing выполняет стандартные операции очистки буфера глубины и цвета, очищение объектно-видовой матрицы, после чего вызывается функция, отвечающая за визуализацию рисунка.
Вот и всё.

Теперь прейдем к созданию «ядра» или «движка» нашей программы.

4. Создание дополнительных классов и их базовых методов.

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

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

Подключаемые пространства имен:

Теперь переключимся на добавление в исходный код еще двух классов: anLayer и anBrush.

Начнем с anBrush. Разместите код этого класса перед кодом заготовки класса anEngine. Пока он будет не очень разнообразным: вся его функциональность будет сводиться к хранения маски кисти. Причем на данном этапе (создания заготовки всех классов и настройки их работоспособности) мы занесем в конструктор стандартную кисть в виде крестика.

Код класса anBrush:

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

Кисть, которую мы задаем по умолчанию кодом:

Легче представить и понять смысл этого кода, изучив рисунок полученного массива чисел (рис. 4).
Рисунок 4. Принцип, по которому задается маска кисти.
Теперь рассмотрим класс anLayer. Его задачей будет хранение данных закрашенных пикселей одного слоя изображения. Помимо хранения графических данных, слой также содержит алгоритм рисования на основе выбранной кисти: когда пользователь пытается что-либо нарисовать на окне, зажав левую клавишу мыши и перемещая курсор по элементу AnT, оболочка получает событие мыши, которое обрабатывается (корректируется координата Y) и передается в экземпляр класса anEngine с помощью функции Drawing. Этот код мы уже рассмотрели.

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

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

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

Код очень подробно прокомментирован и довольно прост. Поясним лишь функцию рисования: в нее передаются в качестве параметров кисть и координаты, в которых должно пройти рисование.
Так как кисть в ширину и высоту может быть больше одного пикселя, нам необходимо определить середину кисти (для этого мы получаем ее ширину и высоту, через свойства Width и Heigth элемента myBrush (который, кстати, является public элементом).

Далее мы вычитаем из координаты, где должно произойти рисование, половину ширины кисти для оси X и половину высоты для оси Y. При этом мы можем получить отрицательные значения, если, например, кисть будет шириной 10 пикселей, нам, следовательно, надо будет сместиться на 5 пикселей влево, чтобы начать алгоритм рисования кистью. Но что если при этом координата X точки, где должно пройти рисование будет равна, к примеру, 2? Тогда мы получим отрицательно индекс элемента массива, откуда должно начаться рисование и получим ошибку.

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

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

Код класса anEngine:

Как видите, некоторые функции объявлены, но не несут никакой функциональной нагрузки.

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

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

Но даже такой, на первый взгляд, недописанный код уже создает минимальный объем объектной модели программы, который заставит ее работать. На рисунке 5 вы можете видеть пример работы программы.
Рисунок 5. Проверка работы основы 2D растрового редактора.
Откомпелировав приложение, вы также можете удостовериться в работе функции рисования.

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

Примечания

Если вы используете Visual Studio 2010, то вам необходимо добавить строку:

для возможности использования ArrayList в вашем приложении.

Уроки OpenGL

Урок 5. Текстурированный куб

  • Что такое UV координаты
  • Как самому загружать текстуры
  • Как использовать текстуры в OpenGL.
  • Что такое фильтрация текстур и мипмаппинг, и как этим пользоваться.
  • Как еще лучше загружать текстуры с помощью GLFW
  • Что такое альфа-канал

UV координаты

Загрузка .BMP файла своими руками

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

GLuint Texture = loadBMP_custom(«uvtemplate.bmp»);

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

  • хорошо : 128*128*, 256*256, 1024*1024, 2*2…
  • плохо : 127*128, 3*5, …
  • хорошо, но странно : 128*256

Использование текстур в OpenGL


  • Фрагментный шейдер нуждается в UV координатах
  • Нам нужен так называемый сэмплер(sampler2D), чтобы шейдер знал из какой текстуры извлекать цвет
  • Доступ к цвету фрагмента из текстуры происходит с помощью функции texture() которая возвращает нам цвет в формате (R,G,B,A) vec4. Вскоре мы рассмотрим что такое A компонента.

Фильтрация, Мипмаппинг, и как этим всем пользоваться

Но у нас есть в запасе несколько фокусов которые помогут улучшить картинку.

Линейная фильтрация

С такой фильтрацией все становится немножко получше, но есть еще один метод — Анизотропная фильтрация, который дает еще более красивую картинку. Но он значительно медленнее.

Анизотропная фильтрация

Мипмаппинг(MipMaps)

  • Во время загрузки текстуры мы изменяем её размер в два раза, потом результирующую еще в два раза итд пока у нас не получится текстура 1х1 (все промежуточные сохраняем, конечно же).
  • Когда рисуем модель, то видеокарта выбирает наиболее подходящую текстуру среди мипмапов в зависимости от того, каким должен быть тексель по размеру.
  • Делать выборку текселя из подходящего мипмапа можно любым способом, хоть nearest хоть linear хоть anisotropic.
  • Если хочется еще лучшего качества, можно попробовать делать выборку из двух ближайших мипмапов и смешивать полученные цвета.

Загрузка текстуры с помощью GLFW

Сжатые Текстуры

Моя рекомендация — не делайте этого. Есть варианты получше.

Создание сжатых текстур:

  • Скачайте с сайта ATI программу The Compressonator.
  • Загрузите в него картинку которая имеет размер «степень двойки»
  • Сожмите её с помощью алгоритмов DXT1, DXT3 или DXT5. Если хотите узнать про эти алгоритмы больше, почитайте про них на википедии.

На практике использование сжатых текстур повышает производительность процентов на 20.

Обработка изображений при помощи OpenGL и шейдеров

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

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

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

Для начала давайте кратко рассмотрим отдельные технологии, а затем перейдем к рассмотрению программы Glutcam.

Устройства Video4Linux2

Video4Linux2 является интерфейсом, связывающим ядро Linux со многими видеоустройствами, включающими в себя тюнеры и некоторые вебкамеры. (Некоторые устройства все еще используют старую версию драйверов V4L1). Bill Dirks начал разработку API Video4Linux в 1998 году. Код был добавлен в ядро версии 2.5.46 в 2002 году. API V4L2 предоставляет функции для работы с устройствами, формирующими изображения. При помощи V4L2 приложение может открыть устройство, запросить информацию о его возможностях, установить параметры захвата изображения и выбрать выходной формат и метод обработки. Два аспекта очень важны для нас: метод обработки в выходной формат.

Под методом обработки понимается процесс доставки видеопотока от устройства захвата к пользовательскому приложению. Этот процесс менее банален, чем кажется на первый взгляд, потому что драйвер хранит данные в пространстве ядра, а пользовательское приложение — в пользовательском пространстве. Существуют два метода для осуществления доставки данных. Это интерфейс «чтения», при использовании которого для получения данных, производится чтение из файла устройства при помощи функции read() и интерфейс «поточной передачи».

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

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

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

Существует множество форматов, отличающихся способом хранения данных изображения в памяти. Одним из различий является метод хранения компонентов в «плоскостном» («planar») или «упакованном’ («packed») виде. В форматах с компонентами, хранящимися в «плоскостном» виде, значения Y хранятся вместе, а после них следуют блоки значений U и V. В форматах с компонентами, хранящимися в «упакованном» виде, все значения хранятся вместе.

Программа Glutcam принимает формат RGB, оттенки серого, YUV420 с компонентами в «плоскостном» виде и YUV422 с компонентами в «упакованном» виде. Некоторые устройства поддерживают только формат JPEG; Glutcam не работает с JPEG.

OpenGL

Технология V4L2 позволяет получить данные; для вывода изображения мы будем использовать технологию OpenGL. Библиотека OpenGL для обработки графики в реальном времени была выпущена компанией SGI в 1992 году. Brian Paul выпустил библиотеку Mesa, предоставляющую API OpenGL в 1995 году. Mesa позволяла работать с функциями OpenGL в программном режиме. В 2000 году компания SGI выпустила прототип реализации OpenGL под свободной лицензией. В вашей системе должны быть программные компоненты для поддержки OpenGL или от проекта Mesa или вы можете получить их у производителя вашего видеоадаптера. Производители часто выпускают библиотеки для поддержки OpenGL, использующие их аппаратное обеспечение и позволяющие выполнять операции быстрее полностью программных решений. В 2006 году контроль над развитием API перешел к некоммерческому технологическому консорциуму Khronos Group. Сегодня вы можете встретить реализацию OpenGL во всех устройствах, начиная с суперкомпьютеров и заканчивая мобильными телефонами.

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

Эта статья описывает фрагментные шейдеры GLSL.

Программы шейдеров позволяют внести изменения в «конвейер фиксированной функциональности» («fixed functionality pipeline») OpenGL. Фрагментные шейдеры получают исходные данные в виде текстур OpenGL и переменных шейдера (Рисунок 1). Язык шейдеров имеет синтаксис, похожий на синтаксис языка C с аналогичными базовыми типами и структурами, задаваемыми пользователем.

Рисунок 1. Программы фрагментных шейдеров

GLSL является языком C с добавленной поддержкой векторов и типом для доступа к текстурам. Текстуры читаются при помощи типов с названиями, содержащими «Sampler». Так как Glutcam использует двумерные массивы, мы будем использовать тип Sampler2D.

Эта модель также отлично совмещается с OpenGL и механизмом шейдеров (об этом читайте ниже).

При реализации этой модели в рамках механизма работы с шейдерами, параметр input_image является текстурой. OpenGL и механизм шейдеров самостоятельно реализуют циклы. Ваш фрагментный шейдер должен просто выполнять функцию «something(input_image, x, y, . )». Однако, стоит помнить о том, что в ходе итераций циклы не могут взаимодействовать друг с другом. С учетом этого ограничения итерации циклов могут исполняться параллельно: концептуально будет существовать несколько экземпляров вашей программы для формирования каждого пикселя на выходе (Рисунок 2).

Рисунок 2. Множество параллельно работающих фрагментных шейдеров

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

Если ваша проблема выходит за рамки перечисленных параметров, попробуйте применить похожие методы, такие, как технология CUDA от Nvidia или новая технология Open Computing Language (OpenCL). Страница «The General Purpose Graphics Processor Unit (GPGPU)» также описывает некоторые доступные инструменты и техники.

Библиотеки OpenGL Utility Toolkit Library (GLUT) или FreeGLUT

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

Библиотека GLUT предоставляет возможность использовать все эти функции и вы можете разрабатывать код с применением функций обратного вызова для таких задач, как отрисовка окна или обработка событий клавиатуры и мыши. Библиотека GLUT реализует все низкоуровневые функции. Она также позволяет без лишних сложностей реализовать меню. В дополнение к библиотеке GLUT существует библиотека FreeGLUT c более либеральной лицензией (термином GLUT в данной статье обозначаются обе библиотеки). Программа Glutcam использует библиотеку FreeGLUT для выполнения вышеназванных функций.

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

Библитека OpenGL Extension Wrangler Library (GLEW)

Поскольку в развитии технологии OpenGL принимает участие множество групп специалистов, новые возможности не появляются одновременно в разных реализациях. Большинство новых возможностей появляются в виде расширений, предложенных членами рецензионной коллегии в области архитектуры OpenGL (OpenGL Architecture Review Board). Все эти расширения исследуются и проверяются в работе, после чего некоторые из них принимаются большим количеством производителей. Некоторые расширения попадают в основную спецификацию OpenGL. Механизм расширений OpenGL позволяет вашей программе во время выполнения узнать о том, какие возможности поддерживаются вашей библиотекой OpenGL и где расположены эти функции. Библиотека GLEW выполняет эту работу. GLEW также лежит в основе программы glewinfo, которая выводит информацию об используемой реализации OpenGL и поддерживаемых ею возможностях.

Glutcam

Наконец мы добрались до программы Glutcam. Glutcam соединяет вывод устройства V4L2 с окном OpenGL и использует фрагментные шейдеры для обработки изображения (Рисунок 3). В идеальном случае нам должно удастся сохранить ресурсы центрального процессора, использовав видеоадаптер для выполнения всех операций уровня пикселей. Как минимум, видеоадаптер должен производить преобразование выходного формата камеры в формат RGB для вывода. Поскольку я хотел реализовать дополнительные возможности помимо преобразования цветового пространства, Glutcam также поддерживает операцию распознавания контуров объектов. Распознавание контуров является классической операцией обработки изображений. Вы можете использовать меню для указания того, с помощью какого устройства будет выполняться алгоритм распознавания контуров: центрального процессора или видеоадаптера. Вы можете сравнить результаты, сравнив частоту кадров, выводимую Glutcam, или загрузку центрального процессора, которую можно узнать при помощи команды top.

Рисунок 3. Путь данных от камеры до экрана

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

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

Илон Маск рекомендует:  Что такое код filesize

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

Перед тем, как приступить к компиляции Glutcam, стоит удостовериться в том, что программные компоненты FreeGLUT/GLUT, GLEW, OpenGL 2.0, V4L2 (не V4L1) и соответствующие пакеты для разработки установлены в вашей системе. Распакуйте исходный код и рассмотрите директивы сборки в файле Makefile. Исходные коды программы прикреплены к данной статье. Ссылку на архив с ними вы можете найти в конце статьи.

В том случае, если программа вызвана без параметров, она будет выводить тестовые таблицы из оттенков серого разрешением 320×320.

Вам необходимо выяснить, какое устройство является источником изображения (например, -d /dev/videoN), изображения какого размера оно генерирует (-w,-h) и в каком формате они генерируются (-e) в вашей системе.

Нажмите кнопку в окне Glutcam для начала показа изображения с вашей вебкамеры. Пример показан на Рисунке 4.

Рисунок 4. Вид окна

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

Рисунок 5. Вид окна с гистограммой

Используйте правую кнопку мыши по отношению к окну для вызова меню (Рисунок 6). Вы можете выбрать один из алгоритмов обработки изображения для того, чтобы увидеть функцию распознавания контуров объектов в работе (Рисунок 7). Попробуйте оба варианта алгоритма (с использованием центрального процессора и шейдера) и сравните результаты, основываясь на частоте кадров и загрузке центрального процессора, полученной при помощи команды top.

Когда центральный процессор обрабатывает каждый пиксель (при отображении гистограммы или распознавании контуров объектов), частота кадров значительно падает (на 50-70%) и загрузка центрального процессора значительно возрастает (на 100-200%). Но из этих результатов не стоит делать выводов. И аппаратные реализации и реализации OpenGL значительно различаются. Напомню, что исключительно программные реализации OpenGL работают медленнее. Glutcam работает начиная с Mesa 7.6-01; с этой версией вы увидите вывод, но скорее всего не увидите частоту кадров.

Рисунок 6. Параметры распознавания контуров объектов в программе Glutcam

Рисунок 7. Режим распознавания контуров объектов программы Glutcam

Заключение


Давно, в 1990-х годах (когда были распространены системы на базе процессоров семейства 486) мое внимание привлек вопрос в новостных группах Usenet. Автор вопроса интересовался тем, как использовать сопроцессор для ускорения алгоритма трассировки лучей. Лучшим советом было перепрограммирование процессоров цифровой обработки сигналов на звуковой карте. Мысль об использовании звуковой карты для улучшения работы с графикой смутила меня. С программируемыми видеоадаптерами и сопутствующими технологиями, такими, как CUDA и OpenCL вы можете использовать аппаратное обеспечение для работы с графикой с целью ускорения других вычислений.

OpenGL/GLUT вывод простой картинки

встала задача вывести картинку средствами OpenGL/GLUT без особого использования других библиотек. Не долго думая, получился вот такой код (который рисует белое окно):

OpenCV здесь чисто для облегчения загрузки картинки и будет позже заменено. В последствии не планируется даже натягивать текстуру на что-то — просто вывод на экран. Вопрос знатокам — что здесь я забыл сделать для вывода картинки?

Зачем это нужно делать каждый кадр?

В последствии не планируется даже натягивать текстуру на что-то — просто вывод на экран. Вопрос знатокам — что здесь я забыл сделать для вывода картинки?

Как вы собираетесь выводить содержимое текстуры, не «натягивая» ее на что-либо?

Скажем так — мягко говоря, неочевидный мануал. Попробовал скомпилить sample-code отсюда (внизу страницы) — не вышло: мало того, что какие-то нестандартные библиотеки используются (sfml), так еще и код пришлось править. Попробую еще SFML на GLUT переделать, но хз, что выйдет из этого.

Зачем так в 2020 году делать?

А как делают в 2020 году?

Шейдерами, но тебе рано. По теме: glEnable(GL_TEXTURE_2D);

У меня для этого есть готовый сниппет. Очень удобно.

Это ж как надо упороться, чтобы тупую картинку шейдерами рисовать?

Генерируй не тупую сразу на видеокарте.

Скажем так — мягко говоря, неочевидный мануал.

Более чем очевидный и доступный для понимания. Разжевано все по шагам.

мало того, что какие-то нестандартные библиотеки используются (sfml)

Не более нестандартные, чем glut.

Что именно пришлось править?

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

А если хочется склеивать две и больше текстур стык-в-стык, то это лучше как рисовать?

const char для шейдеров в самом начале.

const char для шейдеров в самом начале.

Может const char* ? В любом случае это warning.

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

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

А если хочется склеивать две и больше текстур стык-в-стык, то это лучше как рисовать?

Ну точнее вместо GL_REPEAT в LOAD_TEXTURE — GL_CLAMP_TO_EDGE.

между glBegin и end — это то, как я хочу отображать текстуры. Совсем не гибкий способ, но иных пока не знаю=( Хотя в этом месте гибкость мне понадобится.

Может const char* ? В любом случае это warning.

g++-4.9.2 сказал, что это ошибка и ему не понравилась буква R в начале строки и отсутствие переносов. Более высокую версию компилера не могу использовать из-за завязки одного из фреймворков на железо.

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

Вы уверены, что именно в отсутствии const ошибка?

и ему не понравилась буква R в начале строки и отсутствие переносов.

Ну так строоковые литералы появились в —std=c++11 .

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

Любое изменение gl-стейта — это накладные расходы. Переключение текстуры — это изменение стейта. Если есть возможность использовать атлас, используйте его.

Я не говорил, что в отсутствие const.

В любом случае мы ушли далеко от основной темы. К тому же хочется это всё сделать на чистом Си.

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

текстуры меняться будут. переключать состояние придется.

Я не говорил, что в отсутствие const.

Значит я неправильно понял это.

К тому же хочется это всё сделать на чистом Си.

Да пожалуйста. Никто же не мешает.

Можно обновлять данные в текстуре без переключения текстур.

Можно обновлять данные в текстуре без переключения текстур.

Это может оказаться дороже переключения текстур. Но ТС так и не пояснил задачу.

Ок. Конкретная задача: используя только GL, GLU, GLUT, gcc-4.9.2 написать прогу, которая выводила бы несколько картинок в разном разрешении (высота одинаковая, ширина разная) стык-в-стык друг с другом. Картинки меняются часто (раз 10-20 в секунду) и имеют FullHD разрешение. В будущем их надо будет еще нарезать прямо на видеокарте. В какую сторону копать? Шейдеры, атласы, массивы текстур? Пока что у меня основной вопрос — как отобразить N текстур в разном разрешении стык-в-стык и сделать это по возможности гибко (количество текстур варьируется)?

Картинки меняются часто (раз 10-20 в секунду) и имеют FullHD разрешение.

Создать текстуры размером с fullhd фрейм. Отправлять в видеокарту только те данные, которые будут видны на экране.
Используя shared context можно обновлять данные текстур в отдельном потоке.

Пока что у меня основной вопрос — как отобразить N текстур в разном разрешении стык-в-стык и сделать это по возможности гибко (количество текстур варьируется)?

OpenGL. Урок 5. Создание и наложение текстур.

Как уже понятно из названия статьи, речь пойдет об использовании текстур в OpenGL. В прошлом уроке мы создали куб и добавили возможность вращать его вокруг осей координат. Сегодня давайте попробуем наложить текстуры на грани куба и получить некое подобие игральной кости =)

Итак, возьмем за основу проект из предыдущей статьи, и для начала нужно добавить в него изображения, на базе которых впоследствии мы создадим текстуры. Я не стал особо заморачиваться, просто нарисовал по-быстрому в paint’е 6 картинок, по одной для каждой грани куба �� Получилось вот так:

Тем, кто как и я, использует Qt Creator теперь необходимо добавить эти файлы в проект. Для этого нужно создать пустой файл ресурсов и прописать в нем пути для всех используемых изображений. Не будем в эту тему углубляться, все-таки статья не об этом ��

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

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

Давайте разбираться, что тут происходит…

Первым делом вызываем функцию glGenTextures() – она генерирует специальные имена для каждой из текстур и помещает их в массив, который мы передаем в качестве одного из аргументов. А первый аргумент – количество текстур, которые нам понадобятся (в данном случае у нас 6 граней, значит передаем в функцию аргумент “6”). Эта функция вызывается только один раз, в самом начале функции generateTextures().

И, кстати, не забудьте объявить массив в нашем классе MainScene:

С этим все понятно, переходим дальше. Теперь мы создаем объект класса QImage и загружаем в него наше изображение. Функция glBindTexture делает активной текстуру с номером, соответствующим аргументу функции. В данном случае это текстура для первой грани (textures[0]). Вызовом этой функции мы как бы показываем OpenGL, что дальнейшие действия нужно производить именно с той текстурой, которую мы выбрали. Поэтому вызов glTexImage2D() произведет генерацию текстуры, соответствующей номеру textures[0] (напоминаю, в этом массиве у нас хранятся уникальные номера-идентификаторы для каждой из текстур проекта).

Функция glTexImage2D() принимает на вход кучу аргументов, так что давайте рассмотрим каждый из них в отдельности:

  • первый параметр говорит о том, что мы будем работать с 2D текстурой
  • второй параметр – уровень детализации, у нас 0
  • третий – 3 – число цветовых компонент – RGB
  • четвертый, пятый – ширина, высота текстуры, у нас они определяются автоматически
  • пятый – относится к границе текстуры, его просто оставляем нулевым
  • шестой, седьмой – дают OpenGL информацию о цветах и типе данных исходного изображения
  • и, наконец, седьмой параметр передает OpenGL сами данные изображения, из которых нужно формировать текстуру

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

Эти две функции определяют, как будет выглядеть текстура, если размер оригинальной текстуры больше/меньше (GL_TEXTURE_MAG_FILTER или GL_TEXTURE_MIN_FILTER), чем изображение на экране. При выборе GL_LINEAR текстура будет всегда выглядеть сглаженной.

С созданием теперь мы разобрались окончательно, добавим пару строк в функцию initializeGL():

Как вы уже догадались, работать с текстурами мы будем также через массивы. Каждой вершине грани (квадрата) нужно поставить в соответствие 2D координату текстуры, причем нужно обязательно учесть, что текстуры в OpenGL имеют нормированные координаты, то есть от 0 до 1. В нашем примере текстура квадратная, значит она будет иметь такие координаты:

Рассмотрим код для наложения текстуры на первую (переднюю) грань куба. Для остальных граней все делается аналогично.

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

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

Вот полный проект с примером программы – OpenGLTest_texture

Полный список

В прошлых уроках мы рисовали цветные треугольники. В этом уроке заменим цвет на текстуру. Мы можем взять любую картинку и сказать системе, чтобы она «наложила» эту картинку на треугольник вместо простой заливки цветом.


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

Создание текстуры из картинки

Начнем с того, как нам картинку передать в OpenGL. Для этого нам придется освоить три понятия: texture unit, texture target, texture object.

Texture object – объект текстуры, который хранит в себе текстуру и некоторые ее параметры. Особенности работы с OpenGL таковы, что вы не можете просто так взять и отредактировать этот объект, или использовать его чтобы вывести на экран. Вам необходимо поместить его в определенный слот. И тогда вы сможете этот объект текстуры изменять или использовать в вашем изображении.

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

Каждый большой прямоугольник с подписью GL_TEXTURE (где N = 0,1,2…) – это texture unit. GL_TEXTURE — это имя константы, по которой к нему можно обратиться. Я нарисовал всего три юнита, но их больше.

Каждый маленький прямоугольник внутри большого – это texture target. Его еще можно назвать типом текстуры. И насколько я понял, в OpenGL ES всего два типа:
GL_TEXTURE_2D – обычная двумерная текстура
GL_TEXTURE_CUBE_MAP – текстура развернутого куба. Т.е. это вот такая штука, состоящая из 6-ти квадратов

Мы в этом уроке будем использовать GL_TEXTURE_2D.

Чтобы работать с объектом текстуры, его надо поместить в target какого-либо юнита. Далее наша работа будет идти уже с этим target. А он уже будет менять объект текстуры.

Т.е. чтобы нам использовать какую-либо 2D картинку как текстуру на экране, надо будет проделать следующие шаги.

1) Прочесть картинку в Bitmap

2) Создать объект текстуры

3) Сделать какой-нибудь юнит активным. Все дальнейшие действия по работе с текстурами система будет выполнять в этом юните. По умолчанию, активен юнит GL_TEXTURE0.

4) Поместить созданный объект текстуры (из п.2.) в какой-либо texture target. В наших примерах обычно это будет GL_TEXTURE_2D. Далее по тексту буду использовать именно этот target. Помещаем объект в target, чтобы иметь возможность работать с этим объектом. Теперь все операции, которые хотим проделать с объектом мы будем адресовать в target.

5) Объект текстуры у нас создан, но не настроен. Нам надо сделать две вещи: закинуть в него Bitmap (из п.1.) и настроить фильтрацию. Фильтрация отвечает за то, какие алгоритмы будут использованы если текстуру приходится сжимать или растягивать, чтобы вывести ее на экран.

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

Т.е. для GL_TEXTURE_2D надо указать необходимые режимы фильтрации и передать в него Bitmap. После этого наш объект текстуры будет готов. Можно переходить к шейдеру.

6) Работать с текстурой будет фрагментный шейдер. Именно он отвечает за заполнение фигур пикселами. Только теперь вместо простого цвета он будет определять какую точку из текстуры надо отображать для каждой точки треугольника.

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

Но мы помним, что текстура находится не просто в юните, но еще и в target. Как тогда шейдер поймет, в каком target указанного юнита ему следует искать текстуру? Это будет зависеть от того, какой тип переменной мы используем в шейдере для представления текстуры. Мы в нашем примере будем использовать тип sampler2D. И благодаря этому типу шейдер поймет, что ему надо брать текстуру из target GL_TEXTURE_2D.

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

Использование текстуры

Теперь вторая важная теоретическая часть. Нам нужно понять, как текстура будет «натянута» на объект. Рассмотрим на самом простом примере. У нас есть квадрат, который мы нарисовали с помощью двух треугольников.

Для упрощения я здесь использую только X и Y координаты. Z здесь не важен абсолютно.

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

Текстуру можно представить в таком виде

Т.е. каждая сторона текстуры считается равной 1 (даже если стороны не равны). И используя эти S и T координаты мы можем указать на любую точку текстуры.

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

В нашем примере мы сопоставим координаты вершины квадрата и координаты точки текстуры следующим образом:

левая верхняя вершина (-1,1) -> левая верхняя точка текстуры (0,0)

левая нижняя вершина (-1,-1) -> левая нижняя точка текстуры (0,1)

правая верхняя вершина (1,1) -> правая верхняя точка текстуры (1,0)

правая нижняя вершина (1,-1) -> правая нижняя точка текстуры (1,1)

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

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

Вот так выглядит один треугольник

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

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

Давайте смотреть код, в котором будет реализовано все то, что мы обсудили. Скачивайте исходники и открывайте модуль lesson175_texture.

Сначала посмотрим на класс TextureUtils. В нем есть метод loadTexture. Этот метод принимает на вход id ресурса картинки, а на выходе вернет нам id созданного объекта текстуры, который будет содержать в себе эту картинку. Разберем этот метод подробно.

Методом glGenTextures создаем пустой объект текстуры. В качестве параметров передаем:
— сколько объектов необходимо создать. Нам нужна одна текстура, указываем 1.
— int массив, в который метод поместит id созданных объектов
— offset массива (индекс элемента массива, с которого метод начнет заполнять массив). Тут, как обычно, передаем 0.

Проверяем, если id равен 0, значит что-то пошло не так и объект текстуры не был создан. Возвращаем 0.

Далее идут методы по получению Bitmap из ресурса. Подробно об этом можно почитать в уроках 157-159.

Если Bitmap получить не удалось, то удаляем объект текстуры методом glDeleteTextures. В качестве параметров передаем:
— сколько объектов надо удалить. Нам надо удалить 1 объект.
— массив с id объектов
— offset массива (индекс элемента массива, с которого метод начнет читать массив). Снова 0.

Далее начинается работа с юнитами и target. Методом glActiveTexture делаем активным юнит GL_TEXTURE0, т.е. юнит с номером 0. Теперь все дальнейшие операции будут адресоваться этому юниту. А вот target надо будет указывать в каждой операции.

Методом glBindTexture мы в target GL_TEXTURE_2D помещаем наш объект текстуры, передав туда его id. Заметьте, мы указали только target, без юнита. Потому что юнит мы уже задали одной строкой ранее и система, получив только target, работает с этим target в активном юните.

Методом glTexParameteri мы можем задать параметры объекта текстуры. У этого метода есть три параметра:
— target
— какой параметр будем менять
— значение, которое хотим присвоить этому параметру

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

Существует два параметра фильтрации, которые нам необходимо задать:
GL_TEXTURE_MIN_FILTER — какой режим фильтрации будет применен при сжатии изображения
GL_TEXTURE_MAG_FILTER — какой режим фильтрации будет применен при растягивании изображения

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

Методом texImage2D мы передаем bitmap в объект текстуры. Тут мы указываем target и ранее созданный bitmap. Остальные два параметра оставляем 0, они для нас пока не важны.

Методом recycle мы сообщаем системе, что bitmap нам больше не нужен.

И напоследок снова вызываем метод glBindTexture, в котором в target GL_TEXTURE_2D передаем 0. Тем самым, мы отвязываем наш объект текстуры от этого target.

Т.е. мы сначала поместили объект текстуры в target, выполнили все операции с ним, а затем освободили target. В результате объект текстуры у нас теперь настроен, готов к работе и не привязан ни к какому target.

Смотрим класс OpenGLRenderer. Тут по сравнению с прошлыми уроками есть немного изменений, не касающихся работы с текстурами. Я вынес код по созданию шейдеров и программы в отдельный метод createAndUseProgram. А в метод getLocations я вынес вызовы методов, которые возвращают нам положение переменных в шейдере.

Теперь смотрим новшества, касающиеся текстур. Т.е. что и как мы делаем, чтобы использовать текстуру. Напомню вкратце, что от нас требуется:

1) Создать объект текстуры из картинки
2) Сопоставить вершины треугольника и координаты текстуры, и передать эти данные в шейдер, чтобы он знал как, ему следует наложить текстуру на треугольник.
3) Поместить объект текстуры в target какого-нибудь юнита
4) Передать в шейдер номер юнита, в котором сейчас находится объект текстуры

Смотрим метод prepareData. В массиве vertices мы задаем данные о 4 вершинах, чтобы нарисовать квадрат. Для каждой вершины мы задаем 5 чисел. Первые три – это координаты вершины, а последние две – это координаты соответствующей точки текстуры.

В переменную texture мы помещаем id объекта текстуры, созданного из картинки box.

В методе getLocations обратите внимание на две новые переменные из шейдеров:
a_Texture – это атрибут в вершинном шейдере. В него будем передавать координаты текстуры.
u_TextureUnit – это uniform переменная, в нее будем передавать номер юнита, в который мы поместим текстуру.

В методе bindData сначала передаем координаты вершин в aPositionLocation. Затем передаем координаты текстуры в aTextureLocation. Т.е. из одного массива мы передаем данные в два атрибута. Мы такое уже делали в Уроке 171. Если вдруг забыли, можно там посмотреть, я очень подробно все расписывал.

Методом glActiveTexture мы делаем активным юнит 0. Он и так по умолчанию активный, но вдруг мы где-то в коде это меняли и делали активным какой-нибудь другой юнит. Поэтому на всякий случай выполняем эту операцию.

Методом glBindTexture помещаем объект текстуры в target GL_TEXTURE_2D.

Методом glUniform1i передаем шейдеру информацию о том, что текстуру он сможет найти в юните 0.

В методе onDrawFrame просим систему нарисовать нам треугольники из 4 вершин. В результате будет нарисован квадрат и на него будет «наложена» текстура.

Теперь смотрим на шейдеры.

Сначала вершинный шейдер vertex_shader.glsl. Здесь мы, как и ранее, вычисляем итоговые координаты (gl_Position) для каждой вершины с помощью матрицы. А в атрибут a_Texture у нас приходят данные по координатам текстуры. И мы их сразу пишем в varying переменную v_Texture. Это позволит нам в фрагментном шейдере получить интерполированные данные по координатам текстуры.

Фрагментный шейдер fragment_shader.glsl. В нем у нас есть uniform переменная u_TextureUnit, в которую мы получаем номер юнита, в котором находится нужная нам текстура. Обратите внимание на тип переменной. Напомню, что из приложения мы в эту переменную передавали 0, как integer. А тут у нас какой-то сложный тип sampler2D. Меня это немного запутало поначалу, и пришлось покопать этот момент. В итоге я пришел к выводу, что, когда система передает 0 в шейдер в тип sampler2D, она смотрит в юнит 0 и в sampler2D помещает содержимое текстуры из target GL_TEXTURE_2D.

Т.е. переданное в шейдер число (а нашем случае 0) указывает на какой юнит смотреть. А тип переменной, в которую передано это число (в нашем случае sampler2D) указывает из какого target надо брать текстуру (из 2D target). Естественно это сработает, только, если вы поместили туда текстуру методами glActiveTexture и glBindTexture.

Илон Маск рекомендует:  Модификаторы патэрна

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

Осталось использовать координаты текстуры и саму текстуру, чтобы получить итоговый фрагмент. Это выполнит метод texture2D, и в gl_FragColor мы получим цвет нужной точки из текстуры.

Текстура ровно легла на квадрат. Вернее, части текстуры легли на треугольники и в результате мы видим квадрат.

Частичное использование текстуры

Мы в примере использовали всю текстуру от (0,0) до (1,1). Но это вовсе не обязательно. Мы вполне можем использовать лишь часть.

Давайте рассмотрим такую картинку


В ней содержатся две картинки. А для квадрата нужна только одна, например та, которая находится слева, до 0.5. Чтобы нам ее «наложить» на квадрат, нужно просто поменять сопоставление вершин и точек текстуры. Теперь правые вершины квадрата будут сопоставляться не с правыми углами картинки, а с серединой.

Давайте выведем на экран еще один квадрат с этой левой половиной текстуры

Дополним массив vertices:

К 4-м вершинам мы добавили еще 4. Это тоже квадрат, который будет нарисован повыше первого. Координаты текстур для него соответствуют левой половине текстуры.

Т.к. мы будем использовать еще одну текстуру, надо в классе OpenGLRenderer создать еще одну переменную

В метод prepareData добавим код создания второго объекта текстуры

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

Видим еще один квадрат. Шейдер использовал не всю текстуру, а ее левую половину, т.к. мы указали ему это координатами в массиве vertices.

Напоследок еще немного теории

Несколько юнитов

Зачем может быть нужно несколько юнитов? Бывают случаи, когда фрагментный шейдер должен использовать сразу несколько текстур, чтобы получить итоговый фрагмент. Тогда ему никак не обойтись без нескольких юнитов, в target-ы которых помещены разные текстуры.

Получить кол-во доступных вам юнитов можно методом glGetIntegerv

Где, cnt – это массив int[] из одного элемента. В итоге, int[0] будет хранить в себе кол-во юнитов.

Режимы фильтрации

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

Итак, нам надо текстуру «натянуть» на треугольник. Точка треугольника называется — фрагмент (отсюда и название фрагментного шейдера, который должен отрисовать каждый фрагмент треугольника). А точка текстуры называется — тексель. Когда текстура накладывается на треугольник, их размеры могут не совпадать, и системе приходится подгонять размер текстуры под размер треугольника. Т.е. впихивать несколько текселей в один фрагмент (minification), если текстура больше треугольника. Либо растягивать один тексель на несколько фрагментов (magnification), если текстура меньше. В этом случае применяется фильтрация, чтобы получить итоговый фрагмент.

Есть два основных режима фильтрации изображения:
NEAREST – для каждого фрагмента просто берется ближайший тексель. Работает быстро, но качество хуже.
LINEAR – для каждого фрагмента берется 4 ближайших текселя и рассчитывается их среднее значение. Медленнее скорость, но лучше качество.

В дополнение к фильтрации может быть применен mipmapping. Он для текстуры создает несколько копий разных размеров — от оригинального до совсем минимального. При выполнении фильтрации берется копия текстуры, которая по размерам ближе всего к треугольнику. Это обеспечит лучшее качество и может ускорить процесс, но увеличивает расход памяти, т.к. вам надо держать в памяти несколько уменьшенных копий текстуры. Подробнее можно посмотреть тут.

Чтобы задействовать mipmapping, необходим такой код:

Вызывайте его сразу после того, как поместили bitmap в текстуру. Для гарантированного результата, ваша текстура должна иметь POT (power of two) размеры. Т.е. ширина и высота текстуры должны быть равны какой-либо степени двойки: 1, 2, 4, 8, 16, 32 и т.п. Максимальный размер – 2048. При этом текстура не обязана быть квадратной, т.е. ширина может быть не равна высоте, главное, чтобы оба значения были POT.

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

Второй способ даст лучшее качество, но первый — быстрее.

Эти два способа подбора копий в комбинации с двумя ранее рассмотренными режимами фильтрации дают нам 4 режима фильтрации:

GL_NEAREST_MIPMAP_NEAREST – фильтрация NEAREST, выбор копии MIPMAP_NEAREST. Т.е. выбирается ближайшая копия текстуры, и к ней применяется NEAREST фильтрация.

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

GL_LINEAR_MIPMAP_NEAREST — фильтрация LINEAR, выбор копии MIPMAP_NEAREST. Т.е. выбирается ближайшая копия текстуры, и к ней применяется LINEAR фильтрация.

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

Итого мы получаем 6 возможных режимов фильтрации:
GL_NEAREST
GL_LINEAR
GL_NEAREST_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR_MIPMAP_LINEAR

Первые два применимы и для minification и для magnification. Остальные четыре – только для minification.

Если снова взглянуть на наш код в методе loadTexture класса TextureUtils:

Мы настраиваем два параметра:

GL_TEXTURE_MIN_FILTER – параметр для задания режима фильтрации при minification GL_TEXTURE_MAG_FILTER – параметр для задания режима фильтрации при magnification

В обоих случаях задаем LINEAR фильтрацию.

Как сделать куб

Я в телеграмм-канал сайта скидывал картинку крутящегося ящика и написал, что вы после этого урока сами сможете такое сделать.

Для этого вам нужно будет немного доработать текущий код.

Для начала, отмените все изменения, которые мы внесли в процессе этого урока.

В моем примере есть только одна сторона куба, состоящая из двух треугольников. Вам надо будет дорисовать остальные 5 сторон. Т.е. добавить в массив вершин еще 10 треугольников и правильно сопоставить их с координатами текстуры. Ну и конечно, добавить их отрисовку в onDrawFrame

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

А если хотите сделать поворот, то добавляйте model матрицу и настраивайте поворот вокруг оси Y. О том, как это сделать, мы говорили в прошлом уроке.

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Урок 2 — Рисуем первый треугольник

Рисуем первый треугольник с использованием OpenGL 3.3

Введение

Если вы уже работали с версиями OpenGL младше 3.0, то наверняка вам приходилось использовать конструкции вид glBegin() / glEnd():

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

Расширения

Из первого урока вы узнали как создать контекст с поддержкой OpenGL 3.3, однако одного контекста мало, необходимо еще получить доступ к функционалу OpenGL 3.3 Для этого предусмотрен специальный механизм получения указателей на функции из драйвера видеокарты, который был мельком упомянут в предыдущем уроке, для Windows это функция wglGetProcAddress.

Стоит также оговориться почему нам необходима функция wglGetProcAddress, дело в том что при сборке приложения в Windows нам доступна только библиотека с поддержкой функционала OpenGL 1.1, а все функции из старших версий мы должны загружать уже в процессе работы приложения, динамически. Не во всех ОС это так, например в Linux производители драйверов для видеокарт распространяют библиотеку с поддержкой именно того функционал, который есть в драйвере, его можно подключать уже в момент сборки приложения, статически. Но даже там есть возможность загружать расширения в процессе работы приложения, сделано это для того, чтобы бинарные файлы собранные на одном компьютере могли быть запущены на другом, возможно с совершенно другим драйвером.

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

Динамическая загрузка функция достаточно проста, вы уже видели как используется функция wglGetProcAddress в прошлом уроке — надо создать контекст OpenGL нужной версии и можно приступать к загрузке функций. Для динамической загрузки функций нам понадобится файл с прототипами этих функций glext.h. Этот свободно доступен с сайта opengl.org и с выходом новых версий стандарта OpenGL он обновляется. В архиве с исходниками к этому уроку этот файл уже есть.

Используя прототипы мы определим указатели на функции и после создания контекста с поддержкой OpenGL 3.3 загрузим их динамически. Выглядит это так:

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

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

Шейдеры

Напоминаю, для тех кто забыл, что фиксированный конвейер обработки данных видеокартой был убран из OpenGL версии 3.0 и выше. Теперь такие данные как вершины и пиксели обрабатываются специальном пользовательской программой — шейдером, соответственно вершинным и фрагментным (пиксельным), которые вкупе представляют собой шейдерную программу.

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

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

Шейдеры для OpenGL пишутся с использованием специального языка GLSL, на который имеется спецификация (доступна с сайта opengl.org). Мы будем использовать GLSL 3.30, эта версия GLSL вышла одновременно с выходом стандарта OpenGL 3.3.

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

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

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

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

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

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

Вершинный буфер и его связь с вершинными атрибутами

Для хранения данных о вершинах геометрии в OpenGL существует специальный объект называемый Vertex Buffer Object, коротко VBO. VBO позволяет создать буфер в памяти вдиеокарты и поместить туда необходимые нам данные. Вершинный буфер создается с подсказкой, как часто мы будем менять данные в этом буфере.

Однако создания одного VBO недостаточно, необходимо создать еще один специальный объект Vertex Array Object, коротко VAO. VAO хранит связи между параметрами вершинных атрибутов и источниками данных в VBO. В VAO хранится из какого VBO какие атрибуты берутся, тип и размер этих атрибутов, смещение в буфере VBO до начала данных для этих атрибутов. Есть одно исключение — VBO для хранения индексов, к VAO можно присоединить только один такой VBO, но про индексный буфер будет рассказано в отдельном уроке.

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

В этом уроке мы используем VBO для хранения всего трех вершин для нашего треугольника, менять мы их не собираемся, поэтому данные поместим в VBO всего один раз, при инициализации OpenGL, там же создадим VAO:

Передача атрибутов и юниформов в шейдер


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

В момент вызова функции glLinkProgram каждому атрибуту и юниформу назначается отдельный цифровой индекс, который используется для работы с ними из нашего приложения. Для получения индекса используются функции glGetAttribLocation и glGetUniformLocation для атрибутов и юниформов, соответственно.

У атрибутов есть одна особенность — мы можем использовать функцию glBindAttribLocation для назначения своих индексов атрибутам прежде чем вызвать функцию glLinkProgram. Можно и не устанавливать свои индексы, шейдер это сделает за нас, именно так и сделано в исходных кодах к этому уроку.

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

Матрицы

В данном уроке нам понадобится только одна матрица — перспективная матрица проекции. Она нам необходима, чтобы переводить позиции вершин в однородные координаты (clip space). Раньше установка матрицы проекции делалась достаточно просто, сначала мы говорили, что хотим поменять матрицу проекции командой glMatrixMode(GL_PROJECTION), а потом указывали параметры матрицы проекции, например для создания перспективной матрицы проекции использовалась функция gluPerspective(fov, aspect, znear, zfar).

В OpenGL версии 3.0 и выше отменили работу со стеком матриц OpenGL, теперь необходимо вручную создавать матрицы и передавать их в шейдер для дальнейшего использования. Создать матрицу, подобную той что делает gluPerspective, просто:

В итоге в массиве M у нас будет готовая перспективная матрица. Именно эту матрицу мы будем передавать в шейдер как юниформ projectionMatrix:

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

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

Вывод треугольника

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

Использование VAO

Согласно стандарту OpenGL версии 3.3 операции над вершинными атрибутами без активного VAO более не допускаются (секция E стандарта). Указано, что VAO по-умолчанию (индекс 0) считается устаревшим и при использовании функции glVertexAttribPointer без текущего активного VAO будет выдана ошибка INVALID_OPERATION. На момент написания урока в драйверах ATI именно так и сделано, однако nVidia все еще позволяет пользователю не создавать VAO.

Полезные ссылки

Исходный код

Доступ к исходному коду уроку с проектом для MSVC можно получить двумя способами:

  • Используя SVN lesson02
  • Скачав архив lesson02.zip

Записки программиста

Продолжаем изучение OpenGL: управление камерой при помощи мыши и клавиатуры

23 сентября 2015

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

Изменения в проекте

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

Процедура glfwTerminate будет вызвана обязательно при выходе из скоупа. Неважно, сделаем ли мы return или будет брошено исключение. Благодаря такому подходу мы (1) заметно сокращаем количество кода и (2) существенно понижаем шанс забыть закрыть что-то, когда это стало ненужным. Работает defer, как несложно догадаться, на деструкторах. Здесь можно найти ссылки на статьи, объясняющие, как пишутся такие вещи, информацию о возможном включении подобной фичи в стандарт, и так далее. К сожалению, на момент написания этих строк, CLion подсвечивал последнюю строчку в приведенном коде красным цветом. Судя по тикету, фикс появится в CLion версии 1.2.

Еще одно очень важное изменение заключается в том, что код, отвечающий за компиляцию шейдеров и линковку их в программу, был перенесен в файл utils.cpp. Код же самих шейдеров теперь подгружается из .glsl файлов, а не хардкодится в коде на C++. Это намного удобнее. Плюс, как оказалось, в CLion даже имеется подсветка синтаксиса для GLSL.

Важно! Чтобы наша программа могла найти шейдеры, в свойствах проекта нужно изменить working directory. Run → Edit Configurations… В списке слева находим программу, меняем ей Working directory на ту, в которой лежит каталог shaders. То есть, на корень репозитория.

Куб с разноцветными гранями

Рисование куба эквивалентно рисованию шести квадратов. Рисование же квадрата эквивалентно рисованию двух треугольников. Таким образом, чтобы нарисовать куб с цветными гранями, нужно всего лишь нарисовать в пространстве 12 разноцветных треугольников. Рисовать треугольники мы уже умеем. Вопрос заключается в том, как нарисовать их все разным цветом?

Для этого мы создадим еще один VBO. Только на этот раз вместо координат вершин будем хранить их цвета:

Сошлемся на этот VBO из VAO, взяв свободный массив атрибутов под номером один:

В vertex shader считываем цвет вершин аналогично тому, как это делается для координат:

#version 330 core

layout ( location = 0 ) in vec3 vertexPos ;
layout ( location = 1 ) in vec3 vertexColor ;

out vec3 fragmentColor ;

void main ( ) <
fragmentColor = vertexColor ;
gl_Position = vec4 ( vertexPos , 1 ) ;
>

Заметьте, что цвет присваивается выходному (out) значению fragmentColor. Это значение идет дальше по rendering pipeline, и потому может быть подхвачено и использовано во fragment shader:

#version 330 core

in vec3 fragmentColor ;

void main ( ) <
color = fragmentColor ;
>

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

glEnable(GL_CULL_FACE)

В коде к этому посту есть неприметная, но очень важная строчка:

Этой строчкой мы активируем так называемый face culling. По умолчанию OpenGL рисует полигоны с обеих сторон. Другими словами, если мы развернем треугольник относительно камеры на 180 градусов, он все так же будет виден. Но если вы посмотрите на окружающие вас объекты, то обнаружите, что все они являются объемными, и видите вы только их внешнюю сторону. Возьмем, к примеру, ваш монитор. Вы не можете просунуть голову сквозь экран монитора и посмотреть на монитор изнутри. На нашей сцене пока нет таких сложных объектов, как мониторы, но есть куб. И OpenGL отрисовывает каждый из его полигонов с двух сторон, хотя внутреннюю их сторону в нормальном приложении мы никогда не увидим. Так вот, face culling как раз предназначен для того, чтобы не рисовать полигоны, которые в настоящее время повернуты к камере не той стороной.

Но как определить, какая из сторон является правильной? Оказывается, это можно сделать по порядку, в котором мы задаем вершины полигонов. Возьмем для примера один из полигонов нашего куба, который задается координатами (1,1,1), (-1,1,1) и (1,-1,1). По умолчанию камера смотрит на плоскость XY, «вниз» вдоль оси Z. Таким образом, данный полигон «закручен» против часовой стрелки относительно текущего положения камеры. По умолчанию OpenGL считает именно такую «закрученность» признаком передней стороны. При отрисовке сцены полигоны, смотрящие на камеру задней стороной, отбрасываются.

Зачем такие сложности? Для нашей простой сцены это, пожалуй, не очень-то и нужно. Но для более сложных face culling существенно сокращает время рендеринга. И поэтому лучше сразу учиться делать правильно. Следует однако быть постоянно начеку. Если по ошибке закрутить полигон не в ту сторону, он будет отброшен. В результате на его месте вы увидите дырку.

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

Оказывается, что мы можем контролировать положение камеры, перспективу, а также размещение всех объектов на сцене, просто умножая координаты вершин всех объектов на матрицы. Матрицы эти удобно представлять в виде произведения трех матриц — Model (M), View (V) и Projection (P), отсюда и название этого произведения, MVP. Когда-то давно мы уже разбирались, как задается положение камеры, и что такое перспектива, поэтому далее я буду предполагать, что вы это более-менее себе представляете. Еще более подробное объяснение можно найти здесь: на английском, на русском.

Пример создания матрицы MVP при помощи уже знакомой нам библиотеки GLM:

// поле зрения (Field of View, FOV) в 80 градусов
// отношение w:h = 4:3
// видим между 1 и 100 единицами перед собой
glm :: mat4 P = glm :: perspective ( 80.0f , 4.0f / 3.0f , 1.0f , 100.0f ) ;

// камера находится в точке (0,0,5)
// она смотрит на точку (0,0,0)
// вектор, идущий из центра камеры к ее верху, равен (0, 1, 0)
// то есть, камера расположена горизонтально
glm :: mat4 V = glm :: lookAt ( glm :: vec3 ( 0 , 0 , 5 ) , glm :: vec3 ( 0 , 0 , 0 ) ,
glm :: vec3 ( 0 , 1 , 0 ) ) ;

// модель повернута относительно оси OY на 30 градусов
// по часовой стрелке, если смотреть вдоль этой оси
glm :: mat4 M = glm :: rotate ( 30.0f , 0.0f , 1.0f , 0.0f ) ;

glm :: mat4 MVP = P * V * M ;

Здесь можно найти пояснения касательно выбора правильного FOV и почему у людей в реальной жизни он 180 градусов, а в OpenGL обычно 70-80. Дело в том, что экран попадает в существенно более узкое поле зрения, чем видят наши глаза, и FOV в OpenGL должен соответствовать именно этому более узкому полю. Попробуйте ради интереса увеличить FOV и посмотрите, что произойдет :) Вообще, поскольку экраны у всех разные и находятся на разном расстоянии от глаз, параметры перспективы рекомендуется выносить в настройки программы. Таким образом, каждый пользователь сможет подстроить их под себя.

Объяснение, почему матрица называется MVP, а не PVM, можно найти здесь. Умножение вектора на матрицу MVP эквивалентно умножению вектора на матрицу M, затем на матрицу V, и затем на P. Этот порядок и используют в названии.

Теперь мы хотим умножить координаты рисуемого нами объекта на матрицу MVP. Как это сделать? Вы можете помнить, что для манипулирования координатами вершин предназначен vertex shader. Но нам нужно как-то пробросить нашу матрицу MVP в шейдер. Сказано — сделано:

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

Вот теперь переданную таким образом матрицу MVP можно использовать в vertex shader:

#version 330 core

layout ( location = 0 ) in vec3 vertexPos ;
uniform mat4 MVP ;

void main ( ) <
vec4 temp = vec4 ( vertexPos , 1 ) ;
gl_Position = MVP * temp ;
>

Заметьте, что, в отличие от пробрасывания in/out переменных (так называемые attribute-переменные) из шейдера в шейдер, как мы это делали выше с цветом вершин, uniform-переменные неизменяемы и «глобальны», то есть, доступны во всех шейдерах и одинаковы для все вершин. Attribute-переменные можно указывать разные для разных вершин и менять при передаче от шейдера к шейдеру.

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

Управление камерой с помощью мыши и клавиатуры

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

Илон Маск рекомендует:  Простейший способ создания адаптивной шапки

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

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

int windowW >;
glfwGetWindowSize ( window, & windowW >& windowHeight ) ;

double mouseX, mouseY ;
glfwGetCursorPos ( window, & mouseX, & mouseY ) ;

horizontalAngleRad + = mouseSpeedRad * ( windowW >/ 2 — mouseX ) ;
verticalAngleRad + = mouseSpeedRad * ( windowHeight / 2 — mouseY ) ;

glfwSetCursorPos ( window, windowW >/ 2 , windowHeight / 2 ) ;

Немного тригонометрии позволяет нам определить, где у камеры верх, лево, право, перед и зад:

glm :: vec3 direction (
cos ( verticalAngleRad ) * sin ( horizontalAngleRad ) ,
sin ( verticalAngleRad ) ,
cos ( verticalAngleRad ) * cos ( horizontalAngleRad )
) ;

glm :: vec3 right = glm :: vec3 (
sin ( horizontalAngleRad — 3.14f / 2.0f ) ,
0 ,
cos ( horizontalAngleRad — 3.14f / 2.0f )
) ;

glm :: vec3 up = glm :: cross ( right, direction ) ;


Изменяем позицию камеры:

if ( glfwGetKey ( window, GLFW_KEY_W ) == GLFW_PRESS ) <
position + = direction * deltaTimeSec * speed ;
>

if ( glfwGetKey ( window, GLFW_KEY_S ) == GLFW_PRESS ) <
position — = direction * deltaTimeSec * speed ;
>

if ( glfwGetKey ( window, GLFW_KEY_A ) == GLFW_PRESS ) <
position — = right * deltaTimeSec * speed ;
>

if ( glfwGetKey ( window, GLFW_KEY_D ) == GLFW_PRESS ) <
position + = right * deltaTimeSec * speed ;
>

И получаем матрицу V:

Полную версию класса Camera и его метода getViewMatrix вы найдете в файле utils/camera.cpp. Плюс к возможности двигать камеру я также реализовал постоянное вращение куба вокруг своей оси. За счет этого я мог быстрее проверять, что куб правильно отрисовывается со всех сторон.

Заключение

Полученная в итоге программка выглядит как-то так:

Движение камеры осуществляется при помощи мыши и кнопок WASD. Переключение в режим «все паутинкой» (отрисовка каркасов, wireframes) происходит нажатием Z, а обратно нажатием X. Выход из программы осуществляется нажатием Q.

Полную версию исходного кода к посту вы найдете в этом репозитории. Как обычно, он был проверен на трех машинах, использующих разные GPU и ОС (Linux, MacOS и Windows).

В качестве упражнения попробуйте отключить face culling, запустить программу и нажать Z. Запомните, как сейчас выглядит куб. Затем нажмите X и поместите камеру внутрь куба. Закройте программу, верните face culling и повторите эксперимент. Сравните результаты двух наблюдений и объясните различия.

Вращение, движение, масштабирование и проекции в OpenGL под Lazarus

В OpenGL используются основные три системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от друга направлением оси z: в правосторонней она направлена на наблюдателя, в левосторонней – в глубину экрана. Ось x направлена вправо относительно наблюдателя, ось y – вверх.

Системы координат в OpenGL

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

РАБОТА С МАТРИЦАМИ

Для задания различных преобразований объектов сцены в OpenGL используются операции над матрицами, при этом различают три типа матриц: модельно-видовая, матрица проекций и матрица текстуры. Все они имеют размер 4×4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций определяет, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект.

Умножение координат на матрицы происходит в момент вызова соответствующей команды OpenGL, определяющей координату (как правило, это команда glVertex*)

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

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

Для определения элементов матрицы текущего типа вызывается команда

где m указывает на массив из 16 элементов типа float или double в соответствии с названием команды, при этом сначала в нем должен быть записан первый столбец матрицы, затем второй, третий и четвертый. Еще раз обратим внимание: в массиве m матрица записана по столбцам.

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

МОДЕЛЬНО-ВИДОВЫЕ ПРЕОБРАЗОВАНИЯ

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

Сама матрица может быть создана с помощью следующих команд:

glTranlsate*() производит перенос объекта, прибавляя к координатам его вершин значения своих параметров.

glRotate*() производит поворот объекта против часовой стрелки на угол angle (измеряется в градусах) вокруг вектора (x,y,z).

glScale*() производит масштабирование объекта (сжатие или растяжение) вдоль вектора (x,y,z), умножая соответствующие координаты его вершин на значения своих параметров.

Все эти преобразования изменяют текущую матрицу, а поэтому применяются к примитивам, которые определяются позже. В случае, если надо, например, повернуть один объект сцены, а другой оставить неподвижным, удобно сначала сохранить текущую видовую матрицу в стеке командой glPushMatrix(), затем вызвать glRotate() с нужными параметрами, описать примитивы, из которых состоит этот объект, а затем восстановить текущую матрицу командой glPopMatrix().

Чтобы изображенная фигура выглядела пространственной, систему координат разворачивают вокруг оси X и вокруг оси Y.

ПРОЕКЦИИ

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

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

Второй способ – использование команд glPushMatrix и glPopMatrix:

ПРОГРАММА — ПРИМЕР ВРАЩЕНИЯ КУБА В LAZARUS НА OPENGL

Разместите на форме три компонента

Примечание. Обратите внимание для установки компонента TOpenGLControl смотрите урок «Подключение и работа с OpenGL в Lazarus под Windows».

Пропишите на события создания формы Form1: OnFormCreate, нажатия кнопки Form1: OnButton1Click и работы таймера Form1: OnTimer1Timer следующий код:

Комментарии

Timer1 — выдает ошибку. Использовал IdleTimer1, работает!

unit Unit1;

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
ExtCtrls, OpenGLContext, GL, GLU;

TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
IdleTimer1: TIdleTimer;
Panel1: TPanel;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure IdleTimer1Timer(Sender: TObject);
private
< private declarations >
public
< public declarations >
OpenGLControl1: TOpenGLControl; // Контекст воспроизведения OpenGL
cube_rotation: GLFloat;
Speed: Double;
end;

var
Form1: TForm1;

procedure TForm1.Button1Click(Sender: TObject);
begin

glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, double(width) / height, 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glTranslatef(0.0, 0.0,-6.0);
glRotatef(cube_rotation, 1.0, 1.0, 1.0);

glBegin(GL_QUADS);
glColor3f(0.0,1.0,0.0); // Set The Color To Green
glVertex3f( 1.0, 1.0,-1.0); // Top Right Of The Quad (Top)
glVertex3f(-1.0, 1.0,-1.0); // Top Left Of The Quad (Top)
glVertex3f(-1.0, 1.0, 1.0); // Bottom Left Of The Quad (Top)
glVertex3f( 1.0, 1.0, 1.0); // Bottom Right Of The Quad (Top)
glEnd();
glBegin(GL_QUADS);
glColor3f(1.0,0.5,0.0); // Set The Color To Orange
glVertex3f( 1.0,-1.0, 1.0); // Top Right Of The Quad (Bottom)
glVertex3f(-1.0,-1.0, 1.0); // Top Left Of The Quad (Bottom)
glVertex3f(-1.0,-1.0,-1.0); // Bottom Left Of The Quad (Bottom)
glVertex3f( 1.0,-1.0,-1.0); // Bottom Right Of The Quad (Bottom)
glEnd();
glBegin(GL_QUADS);
glColor3f(1.0,0.0,0.0); // Set The Color To Red
glVertex3f( 1.0, 1.0, 1.0); // Top Right Of The Quad (Front)
glVertex3f(-1.0, 1.0, 1.0); // Top Left Of The Quad (Front)
glVertex3f(-1.0,-1.0, 1.0); // Bottom Left Of The Quad (Front)
glVertex3f( 1.0,-1.0, 1.0); // Bottom Right Of The Quad (Front)
glEnd();
glBegin(GL_QUADS);
glColor3f(1.0,1.0,0.0); // Set The Color To Yellow
glVertex3f( 1.0,-1.0,-1.0); // Bottom Left Of The Quad (Back)
glVertex3f(-1.0,-1.0,-1.0); // Bottom Right Of The Quad (Back)
glVertex3f(-1.0, 1.0,-1.0); // Top Right Of The Quad (Back)
glVertex3f( 1.0, 1.0,-1.0); // Top Left Of The Quad (Back)
glEnd();
glBegin(GL_QUADS);
glColor3f(0.0,0.0,1.0); // Set The Color To Blue
glVertex3f(-1.0, 1.0, 1.0); // Top Right Of The Quad (Left)
glVertex3f(-1.0, 1.0,-1.0); // Top Left Of The Quad (Left)
glVertex3f(-1.0,-1.0,-1.0); // Bottom Left Of The Quad (Left)
glVertex3f(-1.0,-1.0, 1.0); // Bottom Right Of The Quad (Left)
glEnd();
glBegin(GL_QUADS);
glColor3f(1.0,0.0,1.0); // Set The Color To Violet
glVertex3f( 1.0, 1.0,-1.0); // Top Right Of The Quad (Right)
glVertex3f( 1.0, 1.0, 1.0); // Top Left Of The Quad (Right)
glVertex3f( 1.0,-1.0, 1.0); // Bottom Left Of The Quad (Right)
glVertex3f( 1.0,-1.0,-1.0); // Bottom Right Of The Quad (Right)
glEnd();

cube_rotation += 5.15 * Speed;

procedure TForm1.Button2Click(Sender: TObject);
begin
if > begin
> Button2.Caption:=’Вращать автоматически’;
end
else
begin
> Button2.Caption:=’Остановить вращение’;
end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
// Создание контекста воспроизведения OpenGL и привязка его к панели на форме
OpenGLControl1:=TOpenGLControl.Create(Self);
with OpenGLControl1 do begin
Name:=’OpenGLControl1′;
Align:=alClient;
Parent:=Panel1;
end;

Button2.Caption:=’Вращать автоматически’;
Button1.Caption:=’Повернуть’;
end;

procedure TForm1.IdleTimer1Timer(Sender: TObject);
begin
Button1Click(Sender);
end;

Работа с OpenGL — Минимальная программа

Delphi , Графика и Игры , OpenGL

Работа с OpenGL — Минимальная программа

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

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

То есть схема здесь такая — приложение отдает команды машине OpenGL, получая результат на поверхности своего окна. Такая архитектура называется «клиент — серверной», в роли клиента выступает наше приложение, в роли сервера — система OpenGL. Задача сервера — отрабатывать команды клиента. В принципе, сервер может располагаться и не на нашем компьютере, а удаленно, возможно и за тысячи километров от нас. Наше приложение должно передать серверу требуемые для работы данные — контексты и команды на языке сервера. Также важно понимать, что основную работу выполняет не наше приложение, а сервер. Наше приложение лишь создает окно — платформу для работы сервера, и передает команды серверу. При грамотном подходе нет разницы, какими средствами мы спроектировали приложение — С или Delphi, скорость воспроизведения целиком зависит от производительности сервера — машины OpenGL. Мы выбрали в качестве средства разработки приложений именно Delphi за выдающуюся скорость компиляции и обаятельность концепций.

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

Мы знаем, что ссылка на контекст устройства — величина типа HDC, для получения которой вызываем функцию GetDC. Ссылке на контекст устройства в Delphi соответствует свойство Canvas.Handle формы, принтера и некоторых компонентов. Теоретически всюду в наших примерах в строках, использующих величину DC типа HDC, вместо DC можно использовать Canvas.Handle. В первых примерах для начинающих это так и сделано. Каков же все-таки смысл контекста устройства, если он и так связан с однозначно определенным объектом — окном, областью памяти или принтером, и зачем передавать дополнительно какую-то информацию об однозначно определенном объекте?

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

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

Win32 Programmer’s Reference фирмы MicroSoft о контексте устройства сообщает следующее:

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

Термин «структура», встретившийся здесь, соответствует записи в терминологии Delphi. Контекст устройства Windows содержит информацию, относящуюся к графическим компонентам GDI, контекст воспроизведения содержит информацию, относящуюся к OpenGL, то есть играет такую же роль, что и контекст устройства для GDI. В частности, эти контексты являются хранилищами состояния системы, например, хранят информацию о текущем цвете карандаша.

Итак, ссылка на контекст устройства содержит характеристики устройства и средства отображения. Именно он знает, как выводить на конкретно это устройство. Упрощенно говоря, получив ссылку на контекст устройства, мы берем в руки простой либо цветной карандаш, или кисточку с палитрой в миллионы оттенков. Сервер OpenGL, прежде чем приступать к работе, также должен определиться, на каком оборудовании ему придется работать. Это может быть скромная персоналка, а может быть и мощная графическая станция. Прежде чем получить контекст воспроизведения, сервер OpenGL должен получить детальные характеристики используемого оборудования. Эти характеристики хранятся в специальной структуре, тип которой — TPixelFormatDescriptor (описание формата пикселя). Формат пикселя определяет конфигурацию буфера цвета и вспомогательных буферов.

Самый частый вопрос, который я получаю в связи с моими уроками, заключается в просьбе указать источники подробной информации об OpenGL на русском. К сожалению, если такие и есть, то мне они неизвестны. Главным нашим подручным станет поставляемый в составе Delphi файл помощи по OpenGL. Систему помощи Delphi для получения хороших результатов необходимо настраивать, если в помощи Delphi найти раздел по OpenGL, он не порадует обилием информации. В разных версиях Delphi настройка помощи выполняется по-разному, потребуются некоторые несложные манипуляции, но мы не будем тратить на это время. Будем использовать самый простой способ — контекстную помощь. Наберите в тексте модуля фразу «PixelFormatDescriptor», нажмите клавишу F1 и Вы получите подробную помощь об этом типе. Точно также мы будем получать помощь обо всех терминах, функциях и командах OpenGL.

Итак, мы получили обширное описание структуры PixelFormatDescriptor. Обращаю внимание, что мы видим раздел помощи MicroSoft, рассчитанной на программистов С и С++, поэтому описание использует термины и стилистику именно этих языков. Так, по традиции Delphi имена типов начинаются с префикса T, но нам не удастся найти помощь по термину TPixelFormatDescriptor. К сожалению, это не единственное неудобство, которое нам придется испытывать. Например, если сейчас мы заглянем в файл windows.pas и найдем описание записи TPixelFormatDescriptor, мы обнаружим, что в файле помощи не указаны некоторые константы, а именно: PFD_SWAP_LAYER_BUFFERS, PFD_GENERIC_ACCELERATED и PFD_DEPTH_DONTCARE. А константа, названная PFD_DOUBLE_BUFFER_DONTCARE, по-видимому, соответствует константе, описанной в модуле windows.pas как PFD_DOUBLEBUFFER_DONTCARE. Наверное, более поздние версии помощи и заголовочного файла исправят этот и другие неточности.

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

В каталоге Beginner/1 Вы найдете проект OpenGL_min.dpr, в котором я привел описание всех полей структуры TPixelFormatDescriptor на русском, в момент их первоначального заполнения. Делается это в процедуре SetDCPixelFormat, вызываемой между получением ссылки на контекст устройства и созданием контекста воспроизведения OpenGL. Посмотрим подробнее, что там делается. Полям структуры присваиваются желаемые значения, затем вызовом функции ChoosePixelFormat осуществляется запрос системе, поддерживается ли на данном рабочем месте выбранный формат пикселя, и вызовом функции SetPixelFormat устанавливаем формат пикселя в контексте устройства. Функция ChoosePixelFormat возвращает индекс формата пикселя, который нам нужен в качестве аргумента функции SetPixelFormat.

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

Обратим внимание на поле структуры «битовые флаги» — dwFlags. То, как мы зададим значение флагов, существенно может сказаться на работе нашего приложения, и наобум задавать эти значения не стоит. Тем более, что некоторые флаги совместно ужиться не могут, а некоторые могут присутствовать только в паре с другими. В этом примере флагам я присвоил значение PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL, то есть сообщаю системе, что я собираюсь осуществлять вывод в окно, и что моя система в принципе поддерживает OpenGL. Я ограничился всего двумя константами из обширного списка, приведенного в модуле windows.pas, по каждой из которых в файле помощи приведено детальное описание.

Так, константа PFD_DOUBLEBUFFER включает режим двойной буферизации, когда вывод осуществляется не на экран, а в память, затем содержимое буфера выводится на экран. Это очень полезный режим, если в любом примере на анимацию убрать режим двойной буферизации и все команды, связанные с этим режимом, хорошо будет видно мерцание при выводе кадра. Константу PFD_GENERIC_ACCELERATED имеет смысл устанавливать в случае, если компьютер оснащен графическим акселератором. Флаги, заканчивающиеся на «DONTCARE» , сообщают системе, что соответствующий режим может иметь оба значения, то есть PFD_DOUBLE_BUFFER_DONTCARE — запрашиваемый формат пикселя может иметь оба режима — одинарной и двойной буферизации. Со всеми остальными полями и константами я предоставляю Вам возможность разобраться самостоятельно, только замечу, что поле iLayerType, описанное в windows.pas типа Byte, может, согласно помощи, иметь три значения: PFD_MAIN_PLANE, PFD_OVERLAY_PLANE и PFD_UNDERLAY_PLANE, однако константа PFD_UNDERLAY_PLANE имеет значение -1, так что установить такое значение не удастся.

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

В примере битовым флагам задаем все возможные значения одновременно, числовым полям задаем заведомо нереальное значение 64, и смотрим на выбор формата пикселя, сделанным OpenGL. Результат, который Вы получите — выбранный формат пикселя, я предсказать не смогу — он индивидуален для каждой конкретной конфигурации машины и текущих настроек. Возможно, Вы получите в результате, что режим двойной буферизации не будет установлен — напоминаю, многие флаги устанавливаются только в комбинации с другими определенными. Наше приложение позволяет менять параметры формата пикселя и устанавливать его заново. Чтобы видеть, что происходит воспроизведение, небольшая площадка на экране при каждом тестировании окрашивается случайным цветом, используя функции OpenGL. Поэкспериментируйте с этим приложением, например, определите комбинацию флагов для установления режима двойной буферизации. Посмотрите значение числовых полей формата при различной палитре экрана — 16, 24, 32 бита, но не 256 цветов. О выводе при палитре экрана в 256 цветов — отдельный разговор. Это приложение, в частности, дает ответ на вопрос — как определить, оснащен ли компьютер графическим акселератором. Повозившись с этим приложением, Вы найдете ответ на вопрос, на который я Вам ответить не смогу — как надо заполнить структуру TPixelFormatDescriptor для Вашего компьютера. Обратите внимание, что в коде я установил несколько проверок на отсутствие контекста воспроизведения, который может быть потерян по ходу работы любого приложения, использующего OpenGL — редкая, но возможная ситуация в штатном режиме работы системы и очень вероятная ситуация если, например, по ходу работы приложения менять настройки экрана.

Минимальная программа OpenGL

Теперь мы знаем все, что необходимо для построения минимальной программы, использующей OpenGL. Я привел два варианта этой программы — одна построена исключительно на функциях Windows API, другая использует библиотеку классов Delphi (проекты каталогов Beginner/1 и Beginner/2 соответственно).

Взглянем на головной модуль второго проекта. При создании формы задаем формат пикселя, в качестве ссылки на контекст устройства используем значение Canvas.Handle формы. Создаем контекст воспроизведения OpenGL и храним в переменной типа HGLRC. При обработке события OnPaint устанавливаем контекст воспроизведения, вызываем функции OpenGL и освобождаем контекст. При завершении работы приложения удаляем контекст воспроизведения. Для полной академичности можно включить строки, проверяющие, получен ли контекст воспроизведения, и не теряется ли он по ходу работы. Признаком таких ситуаций является нулевое значение переменной hrc. В минимальной программе я просто окрашиваю окно в желтоватый оттенок. Получив помощь по команде glClearColor, Вы можете узнать, что аргументы ее — тройка вещественных чисел в интервале [0;1], задающих долю красного, зеленого и синего составляющих в цвете и еще один, четвертый аргумент, о котором мы поговорим чуть позднее. Этому аргументу я в примере задал значение 1.0. Вообще то, аргументы glClearColor, согласно помощи, имеют неведомый тип GLclampf. Для того, чтобы разобраться с этим типом, отсылаю к строке

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

Строку нашей программы

будем понимать как очистку экрана, фон при этом покрывается заданным цветом.

Проект, построенный только на функциях API, надеюсь, сейчас стал более понятным. Вместо Canvas.Handle используем собственную переменную dc, в обработчике события WM_PAINT реализуем действия, которые Delphi при обычном подходе выполняет за нас. Напоминаю, что для лучшей устойчивости работы обработчик WM_PAINT следовало бы написать так:

А в обработчике WM_DESTROY следует перед PostQuitMessage добавить строку:

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

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

Во всех своих примерах я приписал рекомендацию не запускать проекты, использующие OpenGL под управлением среды Delphi. Дело в том, что часто в таких ситуациях программа аварийно прерывается, выдавая сообщение «access violation -«. Это происходит и в случае самой аккуратной работы с контекстами, и не связано с небрежностью работы программы. Некоторые программисты вину за это возлагают на софтверные драйверы и рекомендуют обновить их. Некоторые утверждают, что дело в Windows 9X, и под NT этого не происходит. Возможно, Вы тоже ничего такого не замечали и не можете взять в толк, о чем я сейчас веду речь. У меня такие окошки вылетают через раз на одном и том же проекте, хотя откомпилированный модуль работает превосходно. Я полагаю, что если драйверы не «глюкуют», когда приложение работает без среды Delphi, дело не только в драйверах.

Вывод на поверхность компонентов

Теоретически функциями OpenGL возможно осуществлять вывод не только на поверхность формы, а и на поверхность любого компонента, если у него имеется свойство Canvas.Handle, для чего при получении контекста воспроизведения необходимо указывать именно его ссылку на контекст устройства, например, Image1.Canvas.Handle. Однако чаще всего это приводит к неустойчивой работе, вывод «то есть, то нет», хотя контекст воспроизведения присутствует и не теряется. Я советую Вам всегда пользоваться выводом исключительно на поверхность окна. OpenGL прекрасно уживается с визуальными компонентами, как видно из примера TestPFD, если же необходимо ограничить размер области вывода, для этого есть стандартные методы, о которых мы обязательно будем беседовать в будущем.

Просто ради интереса приведу пример, когда вывод OpenGL осуществляется на поверхность панели, то есть компонента, не имеющего свойства Canvas. Для этого мы пользуемся тем, что панель имеет отдельное окно, вызываем функцию GetDC с аргументом Panel1.Handle.

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

Для вывода на компонент класса TImage можете записать:

и удалить строки BeginPaint и EndPaint, поскольку TImage не имеет свойства Handle, то есть не создает отдельного окна. Однако вывод на такие компоненты как раз отличается полной неустойчивостью, так что я не гарантирую Вам надежного положительного результата.

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

В конце сегодняшнего разговора я хочу привести еще несколько проектов, появившихся за это время из под моего пера и дополняющих «ЖиЛистую Delphi».

Статья Работа с OpenGL — Минимальная программа раздела Графика и Игры OpenGL может быть полезна для разработчиков на Delphi и FreePascal.

Комментарии и вопросы

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

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL