В прошлом уроке мы научились раскрашивать наши объекты в разные цвета. Но для того, чтобы добиться некого реализма нам потребуется очень много цветов. В прошлый раз, мы раскрашивали вершины треугольника, если мы пойдем тем же путем, то нам понадобится слишком большое количество вершин для вывода картинки. Заинтересовавшихся, прошу под кат.
Меню
- Начинаем
- OpenGL
- Создание окна
- Hello Window
- Hello Triangle
- Shaders
- Текстуры
Программисты и художники предпочитают использовать текстуры. Текстура — это 2D изображение (1D и 3D текстура также существуют), используемое для добавления деталей объекту; считайте, что текстура — это кусок бумаги с картинкой кирпича (к примеру), который наклеен на ваш дом и кажется, что ваш дом сделан из кирпича.
Помимо картинок, текстуры могут хранить большие наборы данных, отправляемых в шейдеры, но мы оставим этот вопрос для другого урока. Ниже вы можете видеть текстуру кирпичной стены прилепленной на треугольник из прошлого урока.
Для того, чтобы привязать текстуру к треугольнику мы должны сообщить каждой вершине треугольника, какой части текстуры принадлежит эта вершина. Каждая вершина, соответственно должна иметь текстурные координаты, ассоциированные с частью текстуры.
Текстурные координаты находятся в промежутке между 0 и 1 по x и y оси (мы же используем 2D текстуры). Получение цвета текстуры с помощью текстурных координат называется отбором (sampling). Текстурные координаты начинаются в (0, 0) в нижнем левом углу текстуры и заканчиваются на (1, 1) в верхнем правом углу. Изображение ниже демонстрирует как мы накладывали текстурные координаты на треугольник:
Мы указали 3 текстурные координаты для треугольника. Мы хотим, чтобы нижний левый угол треугольника соотносился с нижним левым углом текстуры, поэтому мы передаем (0, 0) нижней левой вершине треугольника. Соответственно в нижнюю правую вершину передаем (1, 0). Верхняя вершина треугольника должна соотноситься с центральной частью верхней стороной текстуры, поэтому мы передаем в верхнюю вершину (0.5, 1.0) в качестве текстурной координаты.
В результате текстурные координаты для треугольника должны выглядеть как-то так:
GLfloat texCoords[] = {
0.0f, 0.0f, // Нижний левый угол
1.0f, 0.0f, // Нижний правый угол
0.5f, 1.0f // Верхняя центральная сторона
};
Сэмплинг текстуры может быть выполнен различными методами. Наша работа — сообщить OpenGL как он должен проводить сэмплинг.
Texture Wrapping
Текстурные координаты, зачастую, находятся в промежутке между (0,0) и (1,1), но что произойдет, если текстурные координаты выйдут за этот промежуток? Поведение OpenGL по-умолчанию — повторять изображение (фактически, просто игнорируется целая часть числа с плавающей точкой), но также есть и другие опции:
- GL_REPEAT: Стандартное поведение для текстур. Повсторяет текстуру.
- GL_MIRRORED_REPEAT: Похоже на _GLREPEAT за исключением того, что в этом режиме изображение отражается.
- GL_CLAMP_TP_EDGE: Привязывает координаты между 0 и 1. В результате выходящие за пределы координаты будут привязаны к границе текстуры.
- GL_CLAMP_TO_BORDER: Координаты, выходящие за пределы диапазона будут давать установленный пользователем цвет границы.
Каждая их этих опций по разному отображается при использовании текстурных координат, выходящих за пределы промежутка. Изображение ниже отлично демонстрирует различия:
Каждую из вышепредставленных опций можно установить на оси (s, t (и r если вы используете 3D текстуры), эквивалентны x, y и z) с помощью функций **glTextParameter***:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
Первый аргумент определяет цель к которой привязана наша текстура, мы работаем с 2D текстурой, поэтому наше значение будет GL_TEXTURE_2D. Второе значение требуется для того, чтобы сообщить OpenGL какой конкретно параметр мы хотим установить. Мы хотим настроить опцию WRAP и указать ее значение для осей S и T. В последнем аргументе передается выбранный метод wrapping. В данном случае мы используем GL_MIRRORED_REPEAT.
Если бы мы выбрали GL_CLAMP_TO_BORDER, то нам бы еще пришлось указать цвет границ. Делается это fv альтернативой glTextParameter с передачей в нее GL_TEXTURE_BORDER_COLOR в качестве опции и массива из чисел с плавающей точкой в качестве цветового значения.
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
Фильтрование текстур
Текстурные координаты не зависят от разрешения, но при этом могут принимать любые значения с плавающей точкой, поэтому OpenGL требуется понять какой пиксель текстуры (также называемого текселем) ему требуется наложить. Эта проблема становится наиболее острой если требуется наложить текстуру низкого разрешения на большой объект. Возможно вы уже догадались, что в OpenGL есть опция для фильтрования текстур. Есть несколько доступных опций, но мы обсудим только наиболее важные: GL_NEAREST и GL_LINEAR.
GL_NEAREST (также называемый фильтр ближайшего соседа) — стандартный метод фильтрования в OpenGL. Пока он установлен OpenGL будет выбирать пиксель, который находится ближе всего к текстурной координате. Ниже вы можете видеть 4 пикселя и крест, показывающий текстурную координату. Поскольку центр верхнего левого тексель ближе всего к текстурной координате, то он и выбирается в качестве цвета сэмпла.
GL_LINEAR (также называемый (би)линейной фильтрацией). Принимает интерполированное значение от ближайших к текстурной координате текселей. Чем ближе тексель к текстурной координате, тем больше множитель цвета этого текселя.
Ниже вы можете видеть пример смешивания цветов соседних пикселей:
Но какой же все таки визуальный эффект от выбранного эффекта фильтрования? Давайте посмотрим как эти методы отработают с текстурой в маленьком разрешении на большом объекте (текстура была увеличена для того, чтобы было видно отдельные тексели):
Mipmaps
Представьте, что у вас есть большая комната с тысячами объектов, к каждому из которых привязана текстура. Часть объектов ближе к наблюдателю, часть объектов дальше от наблюдателя и каждому объекту привязана текстура с высоким разрешением. Когда объект находится далеко от наблюдателя, требуется обработать только несколько фрагментов. У OpenGL есть сложности с получением правильного цвета для фрагмента с текстуры высокого разрешения, когда приходится учитывать большое количество пикселей текстуры. Такое поведение будет генерировать артефакты на маленьких объектах, не говоря уже о чрезмерной трате памяти связанной с использованием текстур высокого разрешения на маленьких объектах.
Для решения этой проблемы OpenGL использует технологию, называемую мипмапами (mipmaps), которая предусматривает набор изображений-текстур где каждая последующая текстура вдвое меньше прошлой. Идея, которая лежит в основе мипмапов довольно проста: после определенного расстояния от наблюдателя, OpenGL будет использовать другую мипмап текстуру, которая будет лучше выглядеть на текущем расстоянии. Чем дальше от наблюдателя находится объект тем меньше будет использоваться текстура, поскольку пользователю сложнее будет заметить разницу между разрешениями текстур.Также мипмапы имеют приятное свойство увеличивать производительность, что никогда не бывает лишним. Давайте посмотрим на пример мипмапы поближе:
Создание набора мипмап текстур для каждого изображения довольно муторно, но к счастью OpenGL умеет генерировать их с помощью вызова glGenerateMipmaps после создания текстуры. Скоро мы увидим пример.
Во время переключения между уровнями мипмапов в процессе отрисовки OpenGL может отображать некоторые артефакты, вроде острых краев между двумя уровнями. Также как возможно использование фильтрации на текстурах, также можно использование фильтрации на различных уровнях мипмапов с помощью NEAREST и LINEAR фильтрации для переключения между уровнями. Для указания способа фильтрации между уровнями мипмапами мы можем заменить стандартные методы одной из следующих четырех настроек:
- _GL_NEARESET_MIPMAPNEAREST: Выбирает ближайший мипмап, соотносящийся с размером пикселя и также используется интерполяция ближайшего соседа для сэмплинга текстур.
- _GL_LINEAR_MIPMAPNEAREST: Выбирает ближайший мипмап и сэмплирует его методом линейной интерполяции.
- _GL_NEAREST_MIPMAPLINEAR: Линейная интерполяция между двумя ближайшими мипмапами и сэмплирование текстур с помощью линейной интерполяции.
Также как и с фильтрацией текстур мы можем установить метод фильтрации с помощью функции glTexParameteri
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Частая ошибка — это установка метода фильтрации мипмапов в качестве увеличивающего фильтра. Это не даст никакого эффекта, поскольку мипмапы в основном используют при уменьшении текстуры. Увеличение текстуры не использует мипмапы, поэтому при передаче ему опции фильтрации мипмапа сгенерирует ошибку GL_INVALID_ENUM.
Загрузка и создание текстур
Перед тем как начать использовать наши текстуры нам требуется их загрузить в наше приложение. Текстурные изображения могут храниться в безграничном количестве форматов, в каждом из которых своя структура и упорядоченность данных, так как же мы передадим наше изображение в приложение? Одним из решений является использование удобного нам формата, к примеру .PNG и написать собственную систему загрузки изображений в большой массив байт. Хоть написание собственного загрузчика изображений не представляет собой неподъемную работу, все-таки это довольно утомительно, тем более если вы захотите использовать много форматов файлов.
Другим решением является использование готовой библиотеки для загрузки изображений, которая бы поддерживала множество различных популярных форматов и делала много тяжелой работы за нас. К примеру SOIL.
SOIL
SOIL расшифровывается как Simple OpenGL Image Library, поддерживает большинство популярных форматов изображений, легка в использовании и может быть скачана отсюда. Также как и большинство других библиотек вам придется сгенерировать файл .lib самостоятельно. Вы можете использовать один из их проектов, располагающихся в папке /projects (не волнуйтесь, если версия их проектов будет ниже версии вашей VS. Просто сконвертируйте их в новую версию, это должно работать в большинстве случаев) для создания на его основе собственного. Также добавьте содержимое папки src в свою папку include. Также не забудьте добавить SOIL.lib в настройки своего линковщика и добавить #include <SOIL.h> в начале вашего кода.
Для текущей текстурной секции мы будем использовать изображение деревянного контейнера. Для загрузки изображения через SOIL мы используем функцию SOIL_load_image:
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
Первый аргумент функции — это местоположение файла изображения. Второй и третий аргументы — это указатели на int в которые будут помещены размеры изображения: ширина и высота. Они нам понадобятся для генерации текстуры. Четвертый аргумент — это количество каналов изображения, но мы оставим там просто 0. Последний аргумент сообщает SOIL как ему загружать изображение: нам нужна только RGB информация изображения. Результат будет храниться в большом массиве байт.
Генерация текстуры
Также как и на любой другой объект в OpenGL, на текстуры ссылаются идентификаторы. Давайте создадим один:
GLuint texture;
glGenTexture(1, &texture);
Функция glGenTexture принимает в качестве первого аргумента количество текстур для генерации, а в качестве второго аргумента — массив GLuint в котором будут храниться идентификаторы этих текстур (в нашем случае это один GLuint). Также как любой другой объект мы привяжем его для того, чтобы функции, использующие текстуры, знали какую текстуру использовать.
glBindTexture(GL_TEXTURE_2D, texture);
После привязки текстуры мы можем начать генерировать данные текстуры используя предварительно загруженное изображение. Текстуры генерируются с помощью glTexImage2D:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
У этой функции довольно много аргументов, поэтому давайте по порядку:
- Первый аргумент описывает текстурную цель. Установив значение GL_TEXTURE_2D мы сообщили функции, что наша текстура привязана к этой цели (чтобы другие цели GL_TEXTURE_1D и GL_TEXTURE_3D не будут задействованы).
- Второй аргумент описывает уровень мипмапа для которого мы хотим сгенерировать текстуру, если вдруг мы хотим самостоятельно сгенерировать мипмапы. Поскольку мы оставим генерацию мипмапов на OpenGL мы передадим 0.
- Третий аргумент сообщает OpenGL в каком формате мы хотим хранить текстуру. Поскольку наше изображение имеет только RGB значения то и в текстуры мы также будем хранить только RGB значения.
- Четвертый и пятый аргументы задают ширину и высоту результирующей текстуры. Мы получили эти значения ранее во время загрузки изображения.
- Шестой аргумент всегда должен быть 0. (Аргумент устарел).
- Седьмой и восьмой аргументы описывают формат и тип данных исходного изображения. Мы загружали RGB значения и хранили их в байтах (char) так что мы передаем эти значения.
- Последний аргумент — это сами данные изображения.
После вызова glTexImage2D текущая привязанная текстура будет иметь привязанное к ней изображение. Правда текстура будет иметь только базовое изображение и если мы захотим использовать мипмапы, то нам придется таким же образом задавать изображение просто инкрементируя значение уровня мипмапов. Ну или мы можем просто вызвать glGenerateMipmap после генерации текстуры. Эта функция автоматически сгенерирует все требуемые мипмапы для текущей привязанной текстуры.
После окончания генерации текстуры и мипмапов хорошей практикой является освобождение участка памяти, выделенного под загруженное изображение, и отвязка объекта текстуры.
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
Весь процесс генерации текстуры выглядит примерно так:
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// Устанавливаем настройки фильтрации и преобразований (на текущей текстуре)
...
// Загружаем и генерируем текстуру
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
Применение текстур
Для последующих глав мы будем использовать четырехугольник отрисованный с помощью *glDrawElements из последней части урока про Hello Triangle. Нам надо сообщить OpenGL как сэмплировать текстуру, поэтому мы обновим вершинные данные, добавив в них текстурные координаты:
GLfloat vertices[] = {
// Позиции // Цвета // Текстурные координаты
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Верхний правый
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Нижний правый
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Нижний левый
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // Верхний левый
};
После добавления дополнительных атрибутов нам снова придется оповестить OpenGL о нашем новом формате:
.
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
Заметьте, что мы также скорректировали значение шага прошлых двух атрибутов под 8 * sizeof(GLfloat).
Затем нам потребуется изменить вершинный шейдер для того, чтобы он принимал текстурные координаты в качестве атрибута а затем передавал их фрагментному шейдеру:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
TexCoord = texCoord;
}
Фрагментный шейдер также должен принимать TexCoord в качестве входной переменной.
Фрагментный шейдер также должен иметь доступ к текстурному объекту, но как мы передадим его во фрагментный шейдер? GLSL имеет встроенный тип данных для текстурных объектов, называемый sampler у которого в качестве окончания тип текстуры, тоесть sampler1D, sampler3D и, в нашем случае, sampler2D. Мы можем добавить текстуру фрагментному шейдеру просто объявив uniform smpler2D к которому мы позже передадим текстуру.
#version 330 core
in vec3 ourColor;
in vec2 TexCoord;
out vec4 color;
uniform sampler2D ourTexture;
void main()
{
color = texture(ourTexture, TexCoord);
}
Для сэмплирования цвета текстуры мы используем встроенную в GLSL функцию texture которая в качестве первого аргумента принимает текстурный sampler, а в качестве второго аргумента текстурные координаты. Функция texture затем сэмплирует значение цвета, используя текстурные параметры, которые мы задали ранее. Результатом работы этого фрагментного шейдера будет (фильтрованный) цвет текстуры на (интерполированноый) текстурной координате.
Осталось только привязать текстуру перед вызовом glDrawElements и она автоматически будет передана сэмплеру фрагментного шейдера:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
Если вы все сделали верно то получите следующее изображение:
Если ваш четырехугольник полностью черный или белый значит вы где-то ошиблись. Проверьте шейдерные логи и сравните ваш код с исходным.
Для получения более цветастого эффекта мы можем смешать результирующий цвет текстуры с вершинным цветом. Для смешивания мы просто умножим цвета во фрагментном шейдере.
Color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
У вас должно получиться нечто такое?
Текстурный блок
Возможно вы задаетесь вопросом: “Почему sampler2D переменная является uniform, если мы ей так и не присвоили никакое значение с помощью glUniform?”. С помощью glUniform1i мы можем присвоить значение метоположения текстурному сэмплеру для возможности использования нескольких текстур в одном фрагментном шейдере. Местоположение текстуры чаще называется текстурным блоком. Текстурный блок по умолчанию — 0, который означает текущий активный текстурный блок для того, чтобы нам не требовалось указывать местоположение в прошлой секции.
Основная цель текстурных блоков это обеспечение возможности использования более чем 1 текстуры в нашем шейдере. Передавая текстурные блоки сэмплеру мы можем привязывать несколько текстур за один раз до тех пор, пока мы активируем соотносящиеся текстурные блоки. Также как и glBindTexture мы можем активировать текстуры с помощью glActivateTexture передавая туда текстурный блок, который мы хотим использовать:
glActiveTexture(GL_TEXTURE0); // Активируем текстурный блок перед привязкой текстуры
glBindTexture(GL_TEXTURE_2D, texture);
После активации текстурного блока, последующий вызов glBindTexture привяжет эту текстуру к активному текстурному блоку. Блок _GLTEXTURE0 всегда активирован по-умолчанию, так что нам не требовалось активировать текстурные блоки в прошлом примере.
OpenGL поддерживает как минимум 16 текстурных блоков, которые вы можете получить через GL_TEXTURE0 — GL_TEXTURE15. Они объявлены по-порядку, поэтому вы также можете получить их следующим образом: GL_TEXTURE8 = GL_TEXTURE0 + 8. Это удобно, если вам приходится итерировать через текстурные блоки.
В любом случае нам все еще требуется изменить фрагментный шейдер для принятия другого сэмплера:
#version 330 core
...
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
void main()
{
color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}
Финальный результат — это комбинация двух текстур. В GLSL встроена функция mix которая принимает два значения на вход и интерполирует их на основе третьего значения. Если третье значение 0.0 то эта функция вернет первый аргумент, если 1.0 то второй. Значение в 0.2 вернет 80% первого входного цвета и 20% второго входного цвета.
Теперь нам надо загрузить и создать другую текстуру; вы уже знакомы со следующими шагами. Удостоверьтесь, что вы создали еще один объект текстуры, загрузили изображение и сгенерировали финальную текстуру с помощью glTexImage2D. Для второй текстуры мы используем изображение лица во время изучения этих уроков.
Для того, чтобы использовать вторую текстуру (и первую) нам надо будет немного изменить процедуру отрисовки, привязкой обеих текстур к соответствующим текстурным блокам и указанием к какому сэмплеру относится какой текстурный блок:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
Заметьте, что использовали glUniform1i для того, чтобы установить позицию текстурного блока в uniform sampler. Устанавливая их через glUniform1i мы будем уверены, что uniform sampler соотносится с правильным текстурным блоком. В результате вы должны будете получить следующий результат:
Вероятно вы заметили, что текстура перевернута вверх ногами! Это произошло, поскольку OpenGL представляет координату 0.0 по оси Y снизу изображения, но изображения зачастую имеют координату 0.0 сверху по оси Y. Некоторые библиотеки для загрузки изображений, типа Devil имеют настройки для инвертирования Y оси во время загрузки. SOIL такой настройки лишен. У SOIL есть функция SOIL_load_OGL_texture, которая загружает текстуру и генерирует текстуру с флагом __SOIL_FLAG_INVERT_Y__, который решает нашу проблему. Тем не менее эта функция использует вызовы, недоступные в современной версии OpenGL, поэтому нам придется остановиться на использовании SOIL_load_image и самостоятельной загрузкой текстуры.
Для исправления этой небольшой недоработки у нас есть 2 пути:
- Мы можем изменить текстурные координаты в вершинных данных и перевернуть Y ось (вычесть Y координату из 1)
- Мы можем изменить вершинный шейдер для переворачивания Y координаты, заменив формулу задачи TexCoord на TexCoord = vec2(texCoord.x, 1.0f — texCoord.y);..
Приведенные решения — это маленькие хаки, которые позволяют перевернуть изображение. Эти способы работают в большинстве случаев, но результат всегда будет зависеть от формата и типа выбираемой текстуры, так что лучшее решение проблемы — решать ее на этапе загрузки изображения, приводя ее в формат, понятный OpenGL.
Как только вы измените вершинные данные или перевернете Y ось в вершинном шейдере вы получите следующий результат:
Если вы увидели счастливый контейнер, то вы все сделали правильно. Вы можете сравнить свой код с исходным, а также вершинный и фрагментный шейдеры.
Упражнения
Для лучшего усвоения материала прежде чем приступать к следующему уроку взгляните на следующие упражнения.
- Добейтесь того, чтобы только вершинный шейдер был перевернут, с помощью изменения фрагментного шейдера. Решение
- Поэкспериментируйте с другим методам натягивания текстур, изменяя текстурные координаты в пределах от 0.0f до 2.0f вместо 0.0f до 1.0f. Проверьте, сможете ли вы отобразить 4 улыбающихся рожицы на одном контейнере. Решение, Результат
- Попробуйте отобразить только центральные пиксели текстуры на четырехугольнике так, чтобы единичные пиксели были видны при изменении текстурных координат. Попробуйте установить режим фильтрации __GL_NEAREST__ для того, чтобы было видно пиксели более четко. Решение.
- Используйте uniform переменную в качестве 3 параметра функции mix для изменения коэффициента смешивания двух текстур на лету. Используйте кнопки вверх и вниз для регулирования смешивания. Решение, Фрагментный шейдер
Автор: Megaxela