В прошлых уроках мы рисовали цветные треугольники. В этом уроке заменим цвет на текстуру. Мы можем взять любую картинку и сказать системе, чтобы она «наложила» эту картинку на треугольник вместо простой заливки цветом.
Прежде чем перейдем к практике, нам надо будет обсудить два основных момента при работе с текстурами:
— как из обычной картинки получить текстуру, готовую к работе в OpenGL
— как наложить текстуру на треугольник
- Создание текстуры из картинки
- Использование текстуры
- Частичное использование текстуры
- Несколько юнитов
- Режимы фильтрации
- Как сделать куб
- OpenGL — Работа с картинками
- 5.1 Общие слова
- 5.2 Работа с изображениями
- 5.3 Упражнение: «Фон для игры Arcanoid»
- 5.4 Создаем текстуру в памяти
- 5.5 Повторение текстуры
- 5.6 Упражнение: «Вращаем текстуру»
- 5.7 Текстура на сфере
- 5.8 Упражнение «Текстуру в жизнь»
- 5.9 Текстура на чайнике
- 5.10 Упражнение «Текстуру на тор»
- Как визуализировать текстуру на движущихся кругах с помощью SDL2 и OpenGL в C ++?
- 💡 Видео
Создание текстуры из картинки
Начнем с того, как нам картинку передать в 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 для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Видео:OpenGL - Урок 2 - точка, линия, треугольник, кругСкачать
OpenGL — Работа с картинками
Видео:Текстуры в OpenGLСкачать
5.1 Общие слова
Строить примитивные объекты вы уже научились. Но строить трехмерные сцены с использованием только примитивов вам вряд ли придется, да и выглядят они как-то схематично и тускло. Для того, чтобы их оживить, на примитивы накладывают картинки — текстуры. В качестве фона сцены тоже полезно использовать графическое изображение. Тем самым приложение сделается более живым и интересным. Так что в этой главе мы научимся работать с изображениями.
Видео:OpenGL - Урок 17 - Основы текстурирования. Загрузка текстур из файла.Скачать
5.2 Работа с изображениями
Существует множество графических форматов — bmp, pcx, gif, jpeg и прочие. OpenGL напрямую не поддерживает не один из них. В OpenGL нет функций чтения/записи графических файлов. Но поддерживается работа с массивами пикселей. Вы загружаете графический файл, используя библиотеки других фирм, в память и работаете с ними средствами OpenGL. В массиве данные о пикселях могут располагаться разными способами: RGB, BGR, RGBA; могут присутствовать не все компоненты; каждый элемент массива может занимать один байт, два, четыре или восемь; выравнивание может быть по байту, слову или двойному слову. В общем, форматов расположения данных о графическом изображении в памяти очень много. Я рассмотрю один из них, наиболее часто применяемый, как мне кажется. Информация о каждом пикселе хранится в формате RGB и занимает три байта, выравнивание по байту. В Auxiliary Library есть функция auxDIBImageLoad(LPCSTR), которая загружает в память bmp-файл и возвращает указатель на структуру:
Для простоты я буду пользоваться именно этой функцией. Среди прилагаемых программ вы найдете мою утилиту для загрузки файлов из форматов pcx. Исходный текст этой утилиты абсолютно переносим на любую платформу с компилятором ANSI C.
В OpenGL имеются функции для вывода массива пикселей на экран(glDrawPixels), копирования(glCopyPixels), масштабирования(gluScaleImage). Здесь мы рассмотрим только glDrawPixels. Все остальные функции работы с изображениями устроены похожим образом. Для того, чтобы отобразить графический файл в окне OpenGL, вы должны загрузить его в память, указать выравнивание, установить точку, с которой начинается вывод изображения, и вывести его на экран. Раздобудьте где-нибудь свою фотографию в формате BMP. Можете взять фотографию своей девушки. Создайте новый проект. Объявите глобальную переменную —
Перед вызовом функции auxMainLoop в функции main вставьте строку:
Выравнивание устанавливается вызовом функции glPixelStorei с параметром GL_UNPACK_ALIGNMENT и вторым параметром — целым числом, которое указывает выравнивание. Изображения выводятся прямо на экран. Поэтому все происходит в двухмерных координатах. Позиция, с которой начинается вывод изображения, указывается при помощи функции glRasterPos2d(x,y). Также вы можете установить размер пикселя, вызвав функцию glPixelZoom. Первый параметр этой функции — ширина, второй — высота пикселя. Я вызываю эту функцию с аргументами (1,1), что соответствует нормальному пикселю. Замените (1,1) на (3,2) и вы увидите, как картинка растянется в три раза по горизонтали и в два раза по вертикали. Это случилось, потому что теперь каждый пиксель изображения соответствует прямоугольнику 3х2 в окне. И наконец, вывод осуществляет функция glDrawPixels. Первые два параметра — это ширина и высота. Далее, вы указываете формат, в котором хранится информация в памяти, и тип элементов массива. Последним указывается массив данных. В функцию display вставьте следующий код:
Также в OpenGL имеется функция glBitmap для отображения битовых массивов. Битовый массив — это последовательность байт, которые кодируют картинку из двух цветов. Соответственно, каждый байт кодирует 8 пикселей. Среди прилагаемых программ вы найдете мою утилиту pcx_2bpp. Она читает pcx-файл формата один бит на пиксель и направляет на стандартный вывод массив на языке Си.
Исходный файл смотрите здесь. Исполняемый файл здесь. Моя фотография здесь.
Видео:OpenGL - Урок 48 - Как вывести текст без текстуры за 1 минуту.Скачать
5.3 Упражнение: «Фон для игры Arcanoid»
Найдите красивый фон в формате BMP. Можете опять взять свою фотографию. Подложите этот bmp-файл в качестве фона в игру Arcanoid. Закомментируйте разрешение всех тестов, кроме GL_DEPTH_TEST, в функции main. Возможно, я уже говорил о том, что вы всегда должны помнить, что дополнительные параметры затормаживают создание объекта, поэтому устанавливайте их очень осторожно. Часть из них можно установить в функции main. Другие же лучше устанавливать и отменять непосредственно при создании объекта в функции display.
Исходный файл смотрите здесь. Исполняемый файл здесь. Звездное небо здесь.
Видео:OpenGL - Урок 43 - Текстурирование с помощью шейдеров. Мультитекстурирование.Скачать
5.4 Создаем текстуру в памяти
Одного вывода изображений недостаточно для создания полноценных трехмерных сцен. Часто возникает потребность накладывать изображение на трехмерные объекты и поворачивать/сдвигать их. Для этих целей существую текстуры. Также текстуры помогут вам покрыть весь объект в виде мозаики. Скажем, когда у вас имеется кирпичная стена, то вам не надо загружать изображение с кучей кирпичей. Достаточно загрузить один кирпич и указать, что эту текстуру нужно размножить по всей плоскости.
Сначала мы разберем создание и наложение текстур на плоскость. Затем рассмотрим наложение текстур на объекты, описанные в секции 4.1. И наконец, на все прочие, созданные из многоугольников; в частности, тор и чайник.
Для того, чтобы наложить текстуру на объект, вы должны:
- Загрузить графический файл в память
- Создать имя-идентификатор текстуры
- Сделать его активным
- Создать саму текстуру в памяти
- Установить параметры текстуры
- Установить параметры взаимодействия текстуры с объектом
- Связать координаты текстуры с объектом
Первое вы уже научились делать в предыдущей секции. Создайте проект с именем Texture. Объявите следующие глобальные переменные:
Переменные photo_tex и space_tex будут служить идентификаторами текстур. А в photo_image и space_image мы загрузим bmp-файлы. Тут нужно отметить, что текстуры в OpenGL должны иметь размер 2n x 2m, где n и m целые числа. Это сделано для ускорения работы, т.к. сжимать или растягивать такие текстуры быстрее и удобней. Вы, конечно, можете загрузить изображение любого другого размера, но его придется масштабировать. На мой взгляд, это неправильно. Результат масштабирования вас может и не устроить. Так что я использую графические файлы с размером, кратным степени двойки. Мне удобнее отредактировать изображение в каком-нибудь графическом пакете, урезать его или наоборот дополнить, чем потом выяснять, почему оно искажается. Впрочем, тут многое зависит от конкретного случая. Художнику, который делает текстуры, все равно какого размера ее делать, поэтому легче попросить его сделать изображение с подходящими размерами. Вставьте следующий код в функцию main.
Картинки возьмите из моей программы — Texture. Фотографию можете взять свою.;-) Только размер ее желательно оставить 512×512.
Теперь вы должны создать имя-идентификатор текстуры. Его нужно создавать, когда у вас в приложении используется более одной текстуры, чтобы была возможность как-то их различать. В противном случае, когда текстура только одна, идентификатор ей не нужен. В следующем примере, при наложение текстуры на сферу, у нас будет ровно одна текстура, и я покажу вызовы каких функций необязательны. А пока, я предполагаю, что вам нужно использовать несколько текстур. Кстати, я просматривал много примеров при написание книги. Среди них были примеры из широко известной Red Book, примеры из MSDN, из интернета и других источников, но все, что касалось текстур, работало только с одной текстурой. Для элементарной программы-примера, конечно, подойдет и одна текстура, а вот для серьезных приложений вряд ли. Мы хотим написать серьезные приложения, поэтому нам придется использовать нескольких текстур. Функция glGenTextures принимает на вход два параметра. Первый указывает количество имен-идентификаторов текстур, которые нужно создать. Второй параметр — указатель на массив элементов типа unsigned int. Количество элементов в массиве должно совпадать с числом, указанным в качестве первого параметра. Например, следующий код создает десять имен текстур.
Хранить идентификаторы текстур в массиве не всегда удобно. Такой способ подходит для хранения задних фонов или типов стен — кирпичная, каменная и т.п. В общем, в массиве хранят те элементы, между которыми есть что-то общее. В нашем случае, два изображения связаны, т.к. используются в одном приложении, поэтому я создал два различных идентификатора. Так что добавьте следующий код в функцию main.
Теперь мы привязываемся к текстуре фотографии, т.е. делаем ее активной. Для этого служит функция glBindTexture. Первый параметр должен быть GL_TEXTURE_2D или GL_TEXTURE_1D. Он показывает, с одномерным или двумерным изображением будем работать. Все примеры здесь касаются двумерных текстур. Для одномерной текстуры я просто не нашел красивого примера. Впрочем, в Red Book есть пример с одномерной текстурой. Там чайник разукрашивают красной лентой. Где взять эти примеры и многое другое смотрите в приложении ‘A’. Второй параметр glBindTexture — идентификатор, который мы создали выше при помощи glGenTextures. Теперь добавьте вызов этой функции в main.
Теперь мы должны создать саму текстуру в памяти. Массив байт в структуре AUX_RGBImageRec не является еще текстурой, потому что у текстуры много различных параметров. Создав текстуру, мы наделим ее определенными свойствами. Среди параметров текстуры вы указываете уровень детализации, способ масштабирования и связывания текстуры с объектом. Уровень детализации нужен для наложения текстуры на меньшие объекты, т.е. когда площадь на экране меньше размеров изображения. Нулевой уровень детализации соответствует исходному изображению размером 2nx2m, первый уровень — 2n-1x2m-1, k-ый уровень — 2n-kx2m-k. Число уровней соответствует min(n,m). Для создания текстуры имеется две функции glTexImage[1/2]D и gluBuild[1/2]DMipmaps.
Основное различие в том, что первая функция создает текстуру одного определенного уровня детализации и воспринимает только изображения , размер которых кратен степени двойки. Вторая функция более гибкая. Она генерирует текстуры всех уровней детализации. Также эта функция не требует, чтобы размер изображения был кратен степени двойки. Она сама сожмет/растянет изображение подходящим образом, хотя возможно окажется, что и не вполне подходящим. Я воспользуюсь функцией glTexImage2D. Первый параметр этой функции должен быть GL_TEXTURE_2D. Второй — уровень детализации. Нам нужно исходное изображение, поэтому уровень детализации — ноль. Третий параметр указывает количество компонентов цвета. У нас изображение хранится в формате RGB. Поэтому значение этого параметра равно трем. Четвертый и пятый параметры — ширина и высота изображения. Шестой — ширина границы; у нас границы не будет, поэтому значение этого параметра — ноль. Далее, седьмой параметр — формат хранения пикселей в массиве — GL_RGB и тип — GL_UNSIGNED_BYTE. И наконец, восьмой параметр — указатель на массив данных. Еще вы должны вызвать функцию glPixelStorei и задать, что выравнивание в массиве данных идет по байту. Добавьте следующий код в функцию main.
Аналогичный результат можно получить, вставив вызов gluBuild2DMipmaps с параметрами, указанными ниже.
Теперь нужно установить параметры текстуры. Для этого служит функция
Первый параметр принимает значение GL_TEXTURE_1D или GL_TEXTURE_2D. Второй — pname — определяется параметр текстуры, который вы будете изменять. И третий параметр — это устанавливаемое значение. Если вы воспользовались gluBuild2DMipmaps вместо glTexImage2D, то вам не надо устанавливать следующие параметры, т.к. уже сформированы текстуры всех уровней детализации, и OpenGL сможет подобрать текстуру нужного уровня, если площадь объекта не совпадает с площадью текстуры. В противном случае, вы должны добавить следующие строки:
Вы указали, что для уменьшения и увеличения текстуры используется алгоритм GL_NEAREST. Это означает, что цветом пикселя объекта, на который накладывается текстура, становится цвет ближайшего пикселя элемента текстуры. Вместо GL_NEAREST можно указать GL_LINEAR, т.е. цвет элемента объекта будет вычисляться как среднее арифметическое четырех элементов текстуры. Имеются еще четыре алгоритма вычисления цвета элемента объекта. Их можно устанавливать, когда вы создали текстуру со всеми уровнями детализации, т.к. применяют алгоритмы GL_NEAREST и GL_LINEAR к одному или двум ближайшим уровням детализации.
Еще вы можете установить взаимодействие текстуры с объектом. Тут имеются два режима при использовании трех компонентов цвета. Первый режим, установленный по умолчанию, когда у вас учитывается цвет объекта и цвет текстуры. Результирующий цвет получается перемножением компонентов цвета текстуры на компоненты цвета объекта. Скажем, если цвет текстуры — (r,g,b), а цвет объекта, на который она накладывается, — (r0,g0,b0), то результирующим цветом будет — (r*r0,g*g0,b*b0). В случае, если цвет объекта черный — (0,0,0), то вы не увидите на нем текстуру, так как она вся будет черной. Второй режим взаимодействия, когда цвет объекта не учитывается. Результирующим цветом будет цвет текстуры. Эти параметры можно установить следующим образом.
По умолчанию, как я уже сказал, является режим GL_MODULATE. Теперь сделайте активной текстуру space_tex. И повторите для нее то же самое. На этом заканчивается создание текстуры. Осталось связать координаты текстуры с координатами объекта. Отредактируйте функцию display так:
Как вы, наверное, догадались, glTexCoord2d сопоставляет координаты текстуры вершинам четырехугольника. Скажу только, что нижний левый угол текстуры имеет координаты (0,0), а верхний правый — (1,1).
Исходный файл смотрите здесь. Исполняемый файл здесь.
Моя фотография здесь. Звездное небо здесь.
Видео:OpenGL - Урок 19 - Создаем локацию. Добавляем освещение, текстуры и объекты.Скачать
5.5 Повторение текстуры
Размножить текстуру на плоскости не составляет большого труда. Давайте немного отредактируем программу из предыдущего раздела. Для того чтобы иметь возможность повторять текстуру, нужно установить параметр GL_REPEAT для ее S и T координат. S-координата текстуры — это горизонтальная координата, T-координата — вертикальная. Второй параметр, который может быть установлен для координат, — GL_CLAMP. Он гарантирует, что текстура не будет размножена. По умолчанию установлено GL_REPEAT. Но я все-таки приведу соответствующий код, чтобы вы представляли, как устанавливать этот параметр. В функцию main добавьте следующие строки:
Теперь отредактируйте функцию display.
Функция glTexCoord привязывает координаты текстуры к вершинам объекта. Как я уже говорил, левый нижний угол текстуры имеет координату (0,0), а правый верхний — (1,1). Если вы указываете в качестве привязки значение больше единицы, то текстура повторяется. В нашем примере, координату (0,0) текстуры мы привязали к левой нижней вершине плоскости с координатой (-4,-4), а координату (3,2) текстуры к правой верхней вершине (4,4). Тем самым, мы получили размножение текстуры по горизонтали в количестве трех штук и по вертикали в количестве двух штук. Другие две вершины мы связали соответствующим образом. Если там указать не те числа, то изображение наклонится.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Моя фотография здесь. Звездное небо здесь.
Видео:OpenGL - Урок 45 - Переход между текстурами. Смешивание текстур ландшафта. Multitexturing.Скачать
5.6 Упражнение: «Вращаем текстуру»
Завращайте плоскость с фотографией вокруг оси X и Y. Также пусть она равномерно колеблется вдоль оси Z от 3 до 7.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Моя фотография здесь. Звездное небо здесь.
Видео:OpenGL #6 - текстурыСкачать
5.7 Текстура на сфере
Здесь я покажу, как работать с одной единственной текстурой и накладывать текстуры на сферы. Создайте новый проект с именем sphere. Добавьте глобальную переменную.
В функции main загрузите изображение и создайте текстуру. Поскольку текстура у нас в этом приложении всего одна, то создавать идентификатор для нее не надо.
Отредактируйте функцию display. Здесь все вам знакомо см. 4.1, кроме gluQuadricTexture. Эта функция разрешает или запрещает наложение текстуры на трехмерный объект. Второй параметр GL_TRUE или GL_FALSE. По умолчанию наложение текстуры запрещено.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Моя фотография здесь.
Видео:OpenGL - Урок 46 - Как создать скайбокс (skybox). Кубические (cubemap) текстуры.Скачать
5.8 Упражнение «Текстуру в жизнь»
Наложите текстуру на цилиндр, конус, диски и частичный диск. В качестве изображения возьмите какую-нибудь мозаику, горошек и т.п., потому как что-нибудь осмысленное будет смотреться не очень хорошо.
Видео:OpenGL - Урок 30 - Как отобразить текст. Самый простой способ.Скачать
5.9 Текстура на чайнике
Текстуру можно наложить на объект любой сложности. Для этого надо разрешить автоматически генерировать координаты текстуры — glEnable(GL_TEXTURE_GEN_S) и glEnable(GL_TEXTURE_GEN_T). Далее, вы должны установить один из трех алгоритмов генерации координат текстур.
- GL_OBJECT_LINEAR
- GL_EYE_LINEAR
- GL_SPHERE_MAP
Алгоритм генерации координат устанавливается с помощью функции glTexGeni. Первый параметр функции указывает тип координаты, для которой будет установлен алгоритм. GL_S -горизонтальная координата, GL_T — вертикальная. Второй параметр этой функции должен быть GL_TEXTURE_GEN_MODE. И третий параметр — один из перечисленных выше алгоритмов. Создайте очередной проект с именем teapot. Отредактируйте функцию main, как в предыдущей программе, где мы накладывали изображение на сферу. Только добавьте там строчку glEnable(GL_AUTO_NORMAL), чтобы чайник лучше выглядел. Этот режим разрешает расчет векторов нормалей, что позволяет получать улучшенные изображения, однако занимает некоторое время. А функцию display отредактируйте, как показано ниже.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Моя фотография здесь.
Конечно, фотография накладывается не самым лучшим образом. А что вы хотели? Машина сама сгенерировать правильно координаты на кривой поверхности не может. Тем не менее, если в качестве текстуры взять изображение в горошек, то оно довольно неплохо ляжет на чайник. Я не великий художник и горошек в PaintBrush’e мне изобразить не удалось. Нет, не подумайте, что я совсем криворукий, просто, горошины у меня никак не хотели строиться в одну линию, поэтому я нарисовал следующую картину — «Клеточки». И наложил эту текстуру на чайник.
Исходный файл смотрите здесь. Исполняемый файл здесь.
Видео:OpenGL #7 - текстурыСкачать
5.10 Упражнение «Текстуру на тор»
Сделайте такую же программу только для тора.
Видео:OpenGL - Урок 23 - Как выбрать мышкой любой объект (ДЗ номер 4* урока 17).Скачать
Как визуализировать текстуру на движущихся кругах с помощью SDL2 и OpenGL в C ++?
Это мой текущий код:
Отмена моей функции
Основной цикл с отображением круга и эллипса
Функция создания круга
Функция создания эллипса
Функция, которая связывает круг и эллипс
Результат компиляции: красные криклы на самом деле движутся на своих собственных голубых эллипсах
Мне бы хотелось, чтобы у каждого круга была своя текстура.
Я уже загрузил SDL2_image и попробовал этот код:
Но ничего не случилось.
Как я могу рендерить текстуры на этих движущихся кругах?
(PS: Моя конечная цель — создать 2D представление нашей солнечной системы.)
Я знаю, как отобразить изображение.
Я делаю это так:
Возможно, было бы легче изменить размер изображения, вырезать его в круглую форму и повернуть его на эллипсе?
(Но я тоже не знаю как это сделать)
💡 Видео
OpenGL - Урок 16 - Основы освещения.Скачать
OpenGL - Урок 12 - Как создать простое меню. Обработка изменения размера окна.Скачать
#2 - Язык Си. OpenGL + SOIL(png). Загружаем и рисуем текстуру.Скачать
OpenGL - Урок 31 - Как отобразить текст, учитывая ширину символа.Скачать
Базовый курс C++ (MIPT, ILab). Lecture 14 (доп). OpenGL и VulkanСкачать
OpenGL - Урок 18 - Смешивание цветов. Прозрачность. Альфаканал.Скачать
OpenGL - Урок 8 - Массив вершин. Массив индексов.Скачать