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

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

  Обновл. 10 Сен 2021  | 

 18710

 ǀ   7 

На этом уроке мы рассмотрим использование текстур в 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, T и R, эквивалентные осям X, Y, Z, если вы используете 3D-текстуры) с помощью функции glTexParameteri():

Рассмотрим детально вызов функции 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-фильтрацию для текстур, масштабируемых с увеличением масштаба. Таким образом, чтобы указать метод фильтрации для обоих вариантов, можно воспользоваться функцией glTexParameteri(). Код должен выглядеть так же, как и для указания метода наложения:

Мипмапы


Представьте себе, что у нас есть большая комната с тысячами предметов, каждый из которых имеет заданную текстуру. В этой комнате будут находиться далекие объекты, которые при этом имеют такую же текстуру высокого разрешения, как и объекты, расположенные близко к зрителю. Поскольку объекты находятся далеко и, вероятно, создают лишь несколько фрагментов, 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 — линейная интерполяция между двумя ближайшими мипмап-текстурами и сэмплирование интерполированного уровня с помощью метода линейной интерполяции.

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

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

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

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

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

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


Библиотека stb_image.h — это очень популярная библиотека загрузки изображений, написанная Шоном Барреттом, и при этом состоящая всего из одного заголовочного файла, которая способна загружать самые популярные форматы файлов (и её легко интегрировать в наш проект(ы)). Скачать stb_image.h можно здесь. Загружаем один единственный заголовочный файл, добавляем его в свой проект как stb_image.h и создаем еще один дополнительный .cpp-файл, например, 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(), — это связать текстуру, и эта функция автоматически присвоит текстуру сэмплеру фрагментного шейдера:

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

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

  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-сэмплер соответствовал правильному текстурному юниту. У вас должен получиться следующий результат:

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

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

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

  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 (33 оценок, среднее: 5,00 из 5)
Загрузка...

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

  1. Пажилой:

    Еще есть вот такой вариант, для размещения 4-х смайликов на одном контейнере, не оптимизированный правда…

    vertexShader.txt

    fragmentShdaer.txt

  2. Saiel:

    У Вас тут текстура лица без альфа канала. Причем, кажется, на github тоже. Пришлось лезть в оригинальную статью

    1. Юрий:

      Спасибо, исправили.

  3. Grave18:

    Интересно, что 3 аргумент в в функции mix, если он отрицателен, создает эффект негатива при смешивании.

  4. Grave18:

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

  5. Арбузик❤❤❤:

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

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

    1. Grave18:

      Как вариант.

Добавить комментарий для Арбузик❤❤❤ Отменить ответ

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