Урок №6. Текстуры в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 8 Июн 2020  | 

 5284

 ǀ   1 

В этом уроке мы рассмотрим использование текстур в OpenGL.

Текстуры в OpenGL

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

В таких случаях художники и программисты обычно предпочитают использовать текстуру. Текстура — это 2D-изображение (замечу, что также существуют 1D и 3D-текстуры), используемое для добавления деталей к объекту; вы можете представить себе текстуру как лист бумаги с красивым узором из кирпичей на нём, аккуратно наложенным поверх вашего 3D-дома, чтобы он выглядел так, как будто ваш дом сделан из кирпича. Данный способ позволяет добавлять множество деталей в одно изображение, а значит, у нас появляется возможность создавать иллюзию чрезвычайно детализированного объекта без необходимости указывать дополнительные вершины.

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

Ниже вы увидите текстурное изображение кирпичной стены:

Которое мы наложим на треугольник из предыдущего урока:

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

Значения текстурных координат лежат в диапазоне от 0 до 1 по соответствующим осям X и Y (напоминаю, что мы используем 2D-текстурные изображения). Получение цвета текстуры с использованием текстурных координат называется сэмплированием. Текстурные координаты отсчитываются от точки (0,0), соответствующей нижнему левому углу текстурного изображения, и до точки (1,1), соответствующей правому верхнему углу текстурного изображения.

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

Мы задаём 3 точки для треугольника. При этом мы хотим, чтобы нижняя левая часть треугольника соответствовала нижней левой части текстуры, поэтому используем текстурную координату (0,0) для нижней левой вершины треугольника. То же самое относится и к нижней правой части с текстурной координатой (1,0). Для вершины треугольника возьмём точку с текстурными координатами (0.5, 1.0). Далее, остаётся только передать 3 текстурные координаты в вершинный шейдер, который затем отправит их во фрагментный шейдер, а тот, в свою очередь, аккуратно интерполирует все текстурные координаты для каждого фрагмента.

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

Текстурное сэмплирование может быть выполнено различными способами. Следовательно, наша задача — это рассказать OpenGL, как он должен выполнить сэмплирование.

Наложение текстур


Как уже говорилось выше, значения текстурных координат обычно лежат в диапазоне от (0,0) до (1,1), но, что произойдёт, если мы зададим координаты вне этого диапазона? По умолчанию, поведение OpenGL заключается в повторении текстурных изображений, но есть и другие варианты:

   GL_REPEAT — поведение, заданное по умолчанию для текстур. Повторяет текстурное изображение.

   GL_MIRRORED_REPEAT — то же самое, что и GL_REPEAT, но отзеркаливает изображение с каждым повторением.

   GL_CLAMP_TO_EDGE — фиксирует координаты между 0 и 1. Результатом является то, что текстурные координаты, значения которых лежат вне данного диапазона, приводятся к интервалу [0, 1], а у текстурного изображения растягиваются края.

   GL_CLAMP_TO_BORDER — координаты вне диапазона закрашиваются заданным пользователем цветом границы.

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

Вышеупомянутые параметры могут быть заданы для каждой координатной оси (оси S, TR, если вы используете 3D-текстуры), эквивалентные осям X, Y, Z) с помощью функции glTexParameter*() :

Рассмотрим детальнее вызов функции glTexParameteri():

   Аргумент №1: С каким типом изображения (1D или 2D) мы собираемся работать; мы пользуемся 2D текстурами, поэтому выбираем GL_TEXTURE_2D.

   Аргумент №2: Для какой оси текстуры будет задаваться выбранный нами режим наложения; мы хотим настроить его как для оси S, так и для оси T.

   Аргумент №3: Указываем режим наложения текстуры; в нашем случае для текущей активной текстуры задан режим GL_MIRRORED_REPEAT.

Если бы мы выбрали режим GL_CLAMP_TO_BORDER, то ещё должны были бы указать цвет границы. Это делается с использованием функции glTexParameterfv() с аргументом GL_TEXTURE_BORDER_COLOR, в котором, используя массив значений типа float, мы передаём значение цвета границы:

Фильтрация текстур

Текстурные координаты не зависят от разрешения, но при этом могут быть любым значением типа с плавающей запятой, а это означает, что OpenGL должен вычислить, к какому текстурному пикселю (также известному как тексель (англ «texel»)) сопоставить нужную текстурную координату. Данный момент становится особенно важным, если у вас есть очень большой объект и текстура с низким разрешением. Вы, вероятно, уже догадались, что мы подошли к проблеме фильтрации текстур, и в OpenGL для этого есть соответствующий набор опций. Вообще говоря, таких опций довольно много, но сейчас мы обсудим наиболее важные из них, а именно: GL_NEAREST и GL_LINEAR.

GL_NEAREST (также известен как «фильтрация с использованием метода ближайших соседей«) — это метод фильтрации текстур, заданный по умолчанию в OpenGL. При использовании режима GL_NEAREST, OpenGL выбирает тексель, центр которого находится ближе всего к текстурной координате. Ниже вы можете увидеть 4 пикселя, где положение крестика представляет собой конкретную текстурную координату. Центр верхнего левого текселя ближе всего расположен к текстурной координате, и поэтому выбирается в качестве сэмплированного цвета:

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

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

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

Также фильтрация текстур может устанавливаться и для операций увеличения или уменьшения (когда происходит увеличение или уменьшения масштаба), поэтому вы можете, например, использовать GL_NEAREST-фильтрацию, когда текстуры масштабируются вниз, и GL_LINEAR-фильтрацию для текстур, масштабируемых вверх. Таким образом, чтобы указать метод фильтрации для обоих вариантов, можно воспользоваться функцией glTexParameter*(). Код должен выглядеть так же, как и для указания метода наложения:

Мипмапы


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

Для решения этой проблемы OpenGL использует концепцию мипмап-текстур (англ. «mipmap» от лат. «multum in parvo» — «много в малом»), которая представляет собой набор текстурных изображений, где каждая последующая текстура в два раза меньше предыдущей. Идея, лежащая в основе мипмап-текстур, очень проста для понимания: после определённого порога расстояния от зрителя, OpenGL будет использовать другую мипмап-текстуру, которая лучше всего подходит для изображения объекта на заданном расстоянии. Поскольку объект находится далеко, меньшее разрешение текстуры не будет заметно пользователю. Благодаря этому, OpenGL сможет сэмплировать правильные тексели, используя меньше кэш-памяти. Давайте поближе рассмотрим то, как выглядят мипмап-текстуры:

Каждая из этих текстур называется мип-уровнем или уровнем детализации.

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

При переключении между мипмап-уровнями во время рендеринга, OpenGL может отобразить некоторые артефакты, такие как: острые края, видимые между двумя мипмап-уровнями. Так же, как и в предыдущем примере с обычной текстурой, мы можем задействовать фильтрацию текстур и между мипмап-уровнями, используя режимы NEAREST и LINEAR при переключении между мипмап-уровнями. Чтобы указать метод фильтрации между мипмап-уровнями, мы можем заменить исходные методы фильтрации одним из следующих четырёх вариантов:

   GL_NEAREST_MIPMAP_NEAREST — принимает ближайшую мипмап-текстуру, которая соответствует размеру пикселя и использует интерполяцию по методу ближайших соседей для текстурного сэмплирования.

   GL_LINEAR_MIPMAP_NEAREST — использует ближайший мипмап-уровень и сэмплирует его с помощью метода линейной интерполяции.

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

   GL_LINEAR_MIPMAP_LINEAR — линейная интерполяция между двумя ближайшими мипмап-текстурами и сэмплирование интерполированного уровня с помощью метода линейной интерполяции.

Так же, как в примере с фильтрацией текстур, мы можем задать способ фильтрации к одному из 4-х вышеупомянутых методов с помощью функции glTexParameteri():

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

Загрузка и создание текстур

Первое, что нам нужно сделать, чтобы непосредственно воспользоваться текстурами, — это загрузить их в наше приложение. Изображения текстур могут храниться в десятках различных форматов файлов, каждый из которых имеет свою собственную структуру и порядок следования данных. Так как же мы собираемся получить эти изображения в нашем приложении? Одним из решений было бы выбрать формат файла, который мы хотели бы использовать, допустим, формат .PNG, и написать собственный загрузчик изображений, чтобы преобразовать формат изображения в большой массив байтов. Хотя написать свой собственный загрузчик изображений не очень сложно, но это всё равно является обременительной задачей, и что делать, если вы захотите добавить поддержку большего количества форматов файлов? Нужно будет писать загрузчик изображений для каждого формата, поддержку которого вы хотите обеспечить.

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

Библиотека stb_image.h


stb_image.h — это очень популярная библиотека загрузки изображений, написанная Шоном Барреттом, и при этом состоящая всего из одного заголовочного файла, которая способна загружать самые популярные форматы файлов (и её легко интегрировать в ваш проект(ы)). Скачать stb_image.h можно здесь. Просто загрузите один единственный заголовочный файл, добавьте его в свой проект как stb_image.h и создайте ещё один дополнительный файл C++, например, stb_image.cpp со следующим кодом:

А дальше в основном файле вашей программы (у меня это main.cpp) добавьте следующую директиву:

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

Чтобы загрузить изображение с помощью stb_image.h мы воспользуемся содержащейся в нём функцией stbi_load():

Первым аргументом функция принимает расположение файла изображения. Затем, в качестве второго, третьего и четвёртого аргументов, — три переменные типа int, которые stb_image.h будет использовать в качестве данных о ширине, высоте и количестве цветовых каналов получившегося изображения. Забегая вперёд, скажу, что нам потребуются ширина и высота изображения для последующего создания текстур.

Генерирование текстур

Как и у любых других, из ранее рассмотренных OpenGL-объектов, у текстуры должен быть ссылающийся на неё идентификатор; давайте создадим его:

Функция glGenTextures() в качестве входных данных принимает количество текстур, которые мы хотим создать, и сохраняет их в массиве типа unsigned int, заданном в качестве второго аргумента (в нашем случае этим аргументом является только одна переменная типа unsigned int). Так же, как и другие объекты, нам нужно связать его для того, чтобы любые последующие текстурные команды взаимодействовали с текущей связанной текстурой:

Теперь, когда текстура связана, мы можем сгенерировать текстуру, используя ранее загруженные графические данные. Текстуры генерируются с помощью функции glTexImage2D():

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

   Аргумент №1: Указываем целевой тип текстуры; в нашем случае, значение GL_TEXTURE_2D означает, что данная операция будет генерировать текстуру для текущего связанного текстурного объекта в том же целевом типе (так, что любые текстуры, привязанные к целевым типам GL_TEXTURE_1D или GL_TEXTURE_3D, не будут затронуты).

   Аргумент №2: Указываем мипмап-уровень, для которого мы хотим создать текстуру, если у вас есть желание вручную задавать каждый мипмап-уровень (но мы оставим его на базовом уровне, который равен 0).

   Аргумент №3: Сообщаем OpenGL, в каком формате мы хотим хранить текстуру. Наше изображение имеет только RGB-значения, поэтому и хранить текстуру мы будем со значениями RGB.

   Аргументы №4-5: Указываем ширину и высоту результирующей текстуры. Мы сохранили их ранее при загрузке изображения, поэтому будем использовать соответствующие переменные.

   Аргумент №6: Должен всегда быть равен 0 (наследие старого кода).

   Аргументы №7-8: Указываем формат и тип данных исходного изображения. Мы загрузили изображение со значениями RGB и сохранили их в виде набора char (байтов), поэтому передадим соответствующие значения.

   Аргумент №9: Фактические данные изображения.

Как только произойдёт вызов функции glTexImage2D(), то связанный в данный момент текстурный объект теперь будет иметь текстуру, прикрепленную к нему. Однако сейчас он имеет только базовый уровень загруженного изображения текстуры, и если мы хотим использовать мипмап-текстуры, то мы должны указать все различные изображения вручную (постоянно увеличивая второй аргумент) или воспользоваться вызовом функции glGenerateMipmap() после создания текстуры. Вызов данной функции автоматически создаст все необходимые мипмап-уровни для текущей связанной текстуры.

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

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

Применение текстур


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

Так как мы добавили дополнительный атрибут вершины, то необходимо снова уведомить OpenGL о новом формате вершинных данных:

В коде:

Обратите внимание, что нам также стоит изменить параметр шага предыдущих двух атрибутов вершин на значение, равное 8 * sizeof(float).

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

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

Фрагментный шейдер также должен иметь доступ к объекту текстуры, но как мы передадим объект текстуры во фрагментный шейдер? В языке GLSL есть встроенный тип данных для объектов текстуры под названием сэмплер, который принимает в качестве постфикса тип текстуры, который нам необходим, например, sampler1D, sampler3D или, в нашем случае, sampler2D. Затем мы можем добавить текстуру во фрагментный шейдер, просто объявляя uniform-переменную sampler2D, которой мы позже присвоим нашу текстуру:

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

Всё, что нам осталось сейчас сделать, перед вызовом функции glDrawElements(), — это связать текстуру, и эта функция автоматически присвоит текстуру сэмплеру фрагментного шейдера:

Если вы всё сделали правильно, то результат должен быть примерно следующим:

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

  Google Drive / Урок №6. Текстуры в OpenGL — Исходный код №1

  GitHub / Урок №6. Текстуры в OpenGL — Исходный код №1

Примечание: Если ваш текстурный код не работает или отображает полностью чёрную текстуру, продолжайте читать далее и переходите к последнему примеру, который должен работать. В некоторых драйверах требуется присвоить текстурный юнит для каждой uniform-переменной сэмплера, что мы обсудим далее в этом уроке.

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

Результат должен быть смесью цвета вершины и цвета текстуры:

Кажется, нашему ящику нравится стиль диско.

Текстурные юниты

Вы, вероятно, задавались вопросом, почему переменная sampler2D является uniform-переменной, если мы даже не присвоили ей какое-либо значение при помощи glUniform(). Используя glUniform1i(), мы фактически можем присвоить location-значение текстурному сэмплеру, чтобы установить сразу несколько текстур во фрагментном шейдере. Такое расположение текстуры называется текстурным юнитом (или ещё «текстурной единицей»). Текстурный юнит, заданный по умолчанию, имеет значение 0, при этом являясь активным по умолчанию текстурным юнитом, поэтому нам не нужно было присваивать location в предыдущем разделе; обратите внимание, что не все графические драйверы присваивают текстурный юнит, заданный по умолчанию, поэтому результат предыдущего примера мог не отображаться у вас.

Основная цель текстурных юнитов — это позволить нам использовать более одной текстуры в наших шейдерах. Назначая текстурные юниты сэмплерам, мы можем связываться сразу с несколькими текстурами, если сначала активируем соответствующий текстурный юнит. Так же как и при использовании функции glBindTexture(), мы можем активировать текстурные юниты с помощью функции glActiveTexture(), передавая в качестве параметра текстурный юнит, который мы хотели бы использовать:

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

Примечание: OpenGL имеет как минимум 16 текстурных юнитов, которые вы можете использовать в своих целях. Их можно активировать с помощью параметров, начиная с GL_TEXTURE0 и до GL_TEXTURE15. Они определены в последовательном порядке, поэтому вы также можете работать с GL_TEXTURE8 через выражение GL_TEXTURE0 + 8. Это будет полезно, когда нам придётся проходиться циклом по нескольким текстурным юнитам.

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

Конечный выходной цвет теперь представляет собой комбинацию двух текстур. Встроенная GLSL-функция mix() принимает два значения в качестве входных данных и линейно интерполирует их на основе своего третьего аргумента. Если третье значение равно 0.0, то возвращается первое входное значение; если оно равно 1.0, то возвращается второе входное значение. Значение 0.2 возвращает 80% от первого входного цвета и 20% от второго входного цвета, что приводит к смешиванию обеих наших текстур.

Сейчас попробуем загрузить и создать другую текстуру; теперь вы должны быть знакомы с этими шагами. Убедитесь, что у вас создан ещё один текстурный юнит, загрузите изображение и сгенерируйте окончательную текстуру с помощью функции glTexImage2D(). Для второй текстуры мы будем использовать изображение вашего выражения лица во время изучения OpenGL:

Код:

Обратите внимание, что теперь мы загружаем .png-изображение, включающее альфа-канал (прозрачность). Это означает, что теперь с помощью GL_RGBA нам нужно указать, что данные изображения также содержат и альфа-канал; в противном случае OpenGL будет неправильно их интерпретировать.

Чтобы использовать вторую текстуру (и первую), нам придётся немного изменить процедуру рендеринга, привязав обе текстуры к соответствующему текстурному юниту:

Мы также должны сообщить OpenGL, к какому текстурному юниту относится каждый сэмплер шейдера, используя вызов функции glUniform1i(). Это необходимо произвести единожды, поэтому сделать это нужно до того, как мы войдём в цикл рендеринга:

Устанавливая сэмплеры через glUniform1i(), мы следим за тем, чтобы каждый uniform-сэмплер соответствовал правильному текстурному юниту. У вас должен получиться следующий результат:

  Google Drive / Урок №6. Текстуры в OpenGL — Исходный код №2

  GitHub / Урок №6. Текстуры в OpenGL — Исходный код №2

Вы, наверное, заметили, что текстура перевернута вверх ногами! Это происходит потому, что OpenGL ожидает, что координата 0.0 на оси Y будет находиться в нижней части изображения, но изображения обычно имеют 0.0 в верхней части оси Y. К счастью для нас, stb_image.h может перевернуть ось Y во время загрузки изображения, для этого нужно добавить следующее выражение перед загрузкой любого изображения:

После того, как вы дали указание для stb_image.h, чтобы она перевернула ось Y при загрузке изображения, вы должны получить следующий результат:

  Google Drive / Урок №6. Текстуры в OpenGL — Исходный код №3

  GitHub / Урок №6. Текстуры в OpenGL — Исходный код №3

Упражнения

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

Задание №1

Сделайте так, чтобы текстура смайлика смотрела в другом/обратном направлении, изменив фрагментный шейдер.

Ответ №1

Задание №2

Поэкспериментируйте с различными методами наложения текстур, указав текстурные координаты в диапазоне от 0.0f до 2.0f, вместо 0.0f до 1.0f. Посмотрите, можно ли отобразить 4 смайлика на одном изображении контейнера, прижатых к его краю:

Ответ №2

Задание №3

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

Ответ №3

Задание №4

Используйте uniform-переменную в качестве третьего параметра функции mix(), чтобы изменить количество видимых текстур. Используйте клавиши со стрелками вверх и вниз, чтобы изменить видимость контейнера или смайлика.

Ответ №4

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (14 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 1

  1. Аватар Арбузик❤❤❤:

    Можно ли было во 2 задании в коде фрагментного шейдера изменить тело main на:

    ?
    Или такой подход нежелателен?

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *