Как наложить текстуру на куб opengl
Перейти к содержимому

Как наложить текстуру на куб opengl

  • автор:

Как наложить текстуру на куб opengl

Урок 6. Наложение текстуры

Из наложения текстуры можно извлечь много полезного. Предположим, что вы хотите, чтобы ракета пролетела через экран. До этого урока мы попытались бы сделать ракету из полигонов и фантастических цветов. С помощью наложения текстуры, мы можем получить реальную картинку ракеты и заставить ее летать по экрану. Как вы думаете, что будет выглядеть лучше? Фотография или объект сделанный их треугольников и четырехугольников? Используя наложение текстуры, и выглядеть будет лучше, и ваша программа будет работать быстрее. Ракета с наложением текстуры — это всего лишь четырехугольник, движущийся по экрану. Ракета сделанная из полигонов может иметь сотни или тысячи полигонов. Отрисовка простого четырехугольника будет отнимать меньше процессорного времени.

Давайте начнем с добавления четырех новых строк в начало кода первого урока. Первые три строки задают четыре вещественных переменных — xrot, yrot и zrot. Эти переменные будут использованы для вращения куба по осям x, y, z. В четвертой строке резервируется место для одной текстуры. Если вы хотите загрузить более чем одну текстуру, измените, число один на число текстур, которые вы хотите загрузить.

Теперь сразу же после этого кода, до InitGL, мы добавим следующую секцию кода. Этот код загружает файл картинки, и конвертирует его в текстуру. Прежде чем я начну объяснять этот код, я сделаю нескольких ВАЖНЫХ замечаний, которые вы должны знать об изображениях, которые вы используете как текстуры. Такое изображение ДОЛЖНО иметь высоту и ширину кратной двум. При этом высота и ширина изображения должна быть не меньше чем 64 пикселя, и по причинам совместимости, не более 256 пикселов. Если изображение, которое вы используете не 64, 128 или 256 пикселов в ширину и высоту, измените его размер в программе для рисования. Имеются возможность обойти эти ограничения, но мы пока будем придерживаться стандартных размеров текстуры.

AUX_RGBImageRec *texture1 задает указатель на структуру для хранения первой картинки, которую мы загрузим и используем как текстуру. Структура содержит красную, зеленную и синею компоненты цвета, которые используются при создании изображения. Обычно так размещается в памяти загруженная картинка. Структура AUX_RGBImageRec определена в библиотеке glAux, и делает возможной загрузку картинки в память. В следующей строке происходит непосредственная загрузка. Файл картинки «NeHe.bmp» из каталога «Data» будет загружен и сохранен в структуре texture1, которую мы задали выше с помощью AUX_RGBImageRec.

Сейчас мы загрузили изображение как данные компонент цветов красного, зеленного и синего, далее мы построим текстуру используя эти данные. Вызовом glGenTextures(1, &texture[0]) мы скажем OpenGL, что мы хотим построить текстуру в нулевом элементе массива texture[]. Помните, в начале урока мы зарезервировали место для одной текстуры с помощью GLuint texture[1]. Хотя вы, возможно, подумали, что мы сохраним текстуру в &texture[1], но это не так. Первая действительная область для сохранения имеет номер 0. Если вы хотите две текстуры, надо задать GLuint texture[2] и вторая текстура будет сохранена в texture[1].

Во второй строке вызов glBindTexture(GL_TEXTURE_2D, texture[0]) говорит OpenGL, что texture[0] (первая текстура) будет 2D текстурой. 2D текстуры имееют и высоту (по оси Y) и ширину (по оси X). Основная задача glGenTexture указать OpenGL на доступную память. В этом случае мы говорим OpenGL, что память доступна в &texture[0]. Затем мы создаем текстуру, и она будет сохранена в этой памяти. Далее, если мы привязываемся к памяти, в которой уже находиться текстура, мы говорим OpenGL захватить данные текстуры из этой области памяти. Обычно это указатель на доступную память, или память, в которой содержиться текстура.

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

В завершении мы создаем фактическую текстуру. В следующей строке мы говорим OpenGL, что текстура будет двухмерной (GL_TEXTURE_2D). Ноль задает уровень детализации, это обычно ноль. Три — число компонент цветовых данных, так как изображение сделано из трех цветовых компонент (красный, зеленный, синий). texture1->sizeX — это ширина текстуры, автоматически. Если вы знаете ширину, вы можете указать ее тут, но проще дать компьютеру сделать это за вас. texture1->sizeY — высота текстуры. Ноль — это бордюр. Он обычно остается нулем. GL_RGB сообщает OpenGL, что данные изображения представлены в порядке следования красных, зеленных и голубых компонент цвета. GL_UNSIGNED_BYTE означает, что данные из которых состоит изображение имеют размер байта и все числа без знака, и в конце texture1->data сообщает OpenGL, где брать сами данные. В этом случае указатель на данные в записи texture1.

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

В первой строке происходит вызов процедуры LoadGLTextures(), которая загружает изображение и делает из него текстуру. Вторая строка glEnable(GL_TEXTURE_2D) разрешает наложение текстуры. Если вы не делаете доступной наложение текстуры, ваш объект будет закрашен сплошным белым цветом, который точно не очень хорош.

Сейчас мы нарисуем куб с текстурой. Мы можете заменить код DrawGLScene на код ниже, или вы можете добавить новый код в оригинальный код первого урока. Эта секция будет сильно прокомментирована, поэтому легка для понимания. Первые две строки кода glClear() и glLoadIdentity() взяты из оригинального кода первого урока. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) очищает экран цветом, который мы выбрали в InitGL(). В этом случае, экран будет очищен в синий цвет. Буфер глубины будет также очищен. Просмотр будет сброшен с помощью glLoadIdentity().

Следующие три строки кода будут вращать куб по оси X, затем по оси Y, и в конце по оси Z. Насколько велико будет вращение (угол) по каждой оси будет зависеть от значения указанного в xrot, yrot и zrot.

В следующей строке кода происходит выбор какую текстуру мы хотим использовать для наложения текстуры. Если вы хотите использовать более чем одну текстуру в вашей сцене, вы должны выбрать текстуру glBindTexture(GL_TEXTURE_2D, texture[номер текстуры для использования]). Вы должны затем нарисовать несколько четырехугольников используя эту текстуру. Каждый раз, когда Вы захотите сменить текстуру, Вы должны привязать новую текстуру. Одно замечание: вы НЕ должны связывать текстуру внутри glBegin() и glEnd().

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

Первый аргумент glTexCoord2f — координата X. 0.0f — левая сторона текстуры. 0.5f — середина текстуры, и 1.0f — правая сторона текстуры. Втрое значение glTexCoord2f — это Y координата. 0.0f — низ текстуры. 0.5f — середина текстуры, и 1.0f — верх текстуры.

Теперь мы знаем, что левая верхняя координата текстуры 0.0f по X и 1.0f по Y, и левая верхняя вершина четырехугольника -1.0f по X, и 1.0f по Y. Теперь осталось сделать так, чтобы оставшиеся три координаты совпали с тремя углами четырехугольника.

Попробуйте поиграться со значениями x и y в glTexCoord2f. Изменение 1.0f на 0.5f будет только рисовать левую половину текстуры от 0.5f (середина) до 1.0f (право).

Сейчас мы увеличим значения xrot, yrot и zrot. Попробуйте изменить значения каждой переменной, замедляя или ускоряя вращение куба, или изменяя ‘ +’ на ‘-‘ заставляя куб вращаться в другом направлении.

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

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

Уроки по OpenGL с сайта OGLDev

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

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

OpenGL предоставляет несколько типов текстур, таких как 1D, 2D, 3D и т.д., которые могут быть использованы в различных ситуациях. У 2D текстуры есть ширина и высота, которые указываются любым положительным целым числом. Умножив ширину на высоту получим количество текселей в текстуре. -Так вы указываете координаты текселя для вершины? -Нет, не совсем. Их будет слишком много, и если потребуется изменить текстуру на другую, имеющую отличные размеры, тогда придется перестроить все вершины. Идея в том, что бы была возможность менять текстуру, не изменяя ее координат. Поэтому координаты текстуры указываются в нормированном отрезке [0,1]. Это значит, что координаты текстур обычно дроби, и умножив эту дробь на ширину / высоту мы получим координаты текселя. Например, если координаты [0.5,0.1], и высота текстуры 320, а ширина 240, то координаты текселя (160,20) (0.5 * 320 = 160 и 0.1 * 200 = 20).

Обычно для текстур используют U и V оси, где U соответствует X и V — Y. OpenGL рассматривает значения UV осей слева направо для U оси снизу вверх для V. Посмотрим на следующее изображение:

Это изображение показывает пространство текстуры, и как вы можете заметить, начало координат в левом нижнем углу. U возрастает направо, а V — вверх. Теперь посмотрим на треугольник, чьи координаты указаны на изображении:

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

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

Другой важный момент в наложении текстур — это ‘фильтрация’. Мы уже говорили о том как получается тексел. Позиция текселя всегда задается в указанном промежутке, но что произойдет, если наши координаты текстуры (запомните — они должны быть в отрезке от 0 до 1) приведут нас к текселю в (152.34,745.14)? Обычно отвечают, что они округлятся до (152,745). Да, это будет работать, и даже даст адекватный результат, но в некоторых случаях будет выглядеть не очень хорошо. Лучше взять 2 на 2 текселя ( (152,745), (153,745), (152,744) и (153,744) ) и совершить некую линейную интерполяцию между их цветами. Эта интерполяция должна сохранить относительное расстояние между (152.34,745.14) и каждым текселем. Чем ближе координата к текселю, тем большее влияние он получит, и наоборот, чем он дальше, тем влияние будет меньше. Это гораздо лучше чем оригинальный подход.

Метод, который выбирает итоговый тексел так же известен как ‘фильтрация’. Простейший подход к округлению координат текстуры известен как ‘ближайшая фильтрация’, а более сложный способ — ‘линейная фильтрация’. Другое название для ближайшей фильтрации — ‘точечная фильтрация’. OpenGL предоставляет несколько типов, и вас есть возможность выбирать. Обычно фильтр, который дает лучший результат, требует больших вычислений от GPU и может повлиять на частоту кадров. Выбор фильтра зависит от качества требуемого результата а так же от способностей целевой платформы.

Теперь, когда мы поняли принцип координат текстуры, пришло время рассмотреть как же реализовано наложение текстур в OpenGL. Текстурирование в OpenGL означает взаимодействие с 4-мя сложно запутанными понятиями: объект текстуры, модуль текстур, сэмплер объекта и uniform-сэмплер в шейдере.

Объект текстуры хранит данные о самом изображении текстуры, иначе говоря, тексели. Текстуры могут быть нескольких типов (1D, 2D и т.д.) с различными размерами, а так же формат данных может различаться (RGB, RGBA и т.д). OpenGL предлагает несколько способов для указания точки начала данных в памяти, типов текстур и загрузки данных в GPU. Существует множество параметров для большего контроля такие как вид фильтра и т.д. Очень схож с вершинным буфером объект текстуры, который то же ассоциируется с указателем. После создания указателя и загрузки данных и параметров вы запросто можете назначать другие указатели на лету, просто передав в состояние OpenGL новую текстуру. Вам больше не требуется загружать данные вновь. С этого момента проверка загружены ли данные в GPU до начала рендера — работа для драйвера OpenGL.

Объект текстуры не обязательно перейдет прямо в шейдер (где фактически находится сэмплер). Вместо этого он перейдет в ‘модуль текстур’, индексы которого передаются в шейдер. Таким образом, шейдер получает текстуру через модуль текстур. Обычно доступно сразу несколько модулей текстур, количество которых зависит от вашей видеокарты. Для того, что бы привязать объект текстуры A к модулю 0 необходимо сначала активировать модуль 0, а затем и привязать объект текстуры A. Вы можете активировать модуль текстур 1 и привязать другой (или даже тот же самый) объект текстуры к нему. Модуль 0 останется привязан к текстуре A.

Возникает небольшая сложность с тем фактом, что каждый модуль текстур на самом деле имеет место для нескольких текстур одновременно из-за того, что текстуры бывают нескольких типов. Это называется ‘позицией’ объекта текстур. Когда мы привязываем объект к модулю, то мы указываем его позицию (1D, 2D и т.д.). Поэтому вы можете иметь привязанный объект A к позиции 1D, а объект B к 2D одного итого же модуля.

Операция выбора (обычно) находится внутри фрагментного шейдера, и для этого существует специальная функция. Функция выбора должна знать модуль текстуры для доступа, поскольку вы можете выбирать из нескольких модулей текстур в фрагментном шейдере. Для этого используется специальная uniform-переменная, согласно позиции текстуры: ‘sampler1D’, ‘sampler2D’, ‘sampler3D’, ‘samplerCube’ и другие. Вы можете создать столько uniform-переменных, сколько захотите и назначить значение модуля текстур для каждой напрямую из приложения. Каждый раз, когда вы вызываете функцию выбора на сэмплер uniform-переменной, соответствующий модуль текстур (а вместе с ним и объект текстур) будет использован.

И последнее понятие — это сэмплер объекта. Не путайте его с сэмплером uniform-переменной! Это разные понятия. Идея в том, что объект текстуры хранит и данные текстуры и параметры, которые настраивают операцию выбора. Эти параметры часть состояния сэмплера. Хотя, вы также можете создать сэмплер объекта, настроить его и привязать к модулю текстуры. Когда вы это сделаете, сэмплер объекта перезапишет все другие состояния сэмплера, определенные в объекте текстур. Не волнуйтесь, пока что мы не будем использовать сэмплеры объектов, но все же лучше знать об их существовании.

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

Прямиком к коду!

OpenGL знает как получить данные текстуры в различных форматах из памяти, но не предоставляет никаких способов для загрузки текстур в память из файлов изображений таких как PNG или JPG. Нам потребуется дополнительная внешняя библиотека что бы сделать это. Их существует большое множество, но мы будем использовать ImageMagick, свободная библиотека, поддерживающая множество типов изображений, и кроме того, она кроссплатформеная. Если вы используете Ubuntu, вы можете легко установить через ‘apt-get install libmagick++-dev’. Если у вам другой Linux дистрибутив, используйте свой менеджер пакетов или скачайте исходники и соберите библиотеку самостоятельно.

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

Во время создания объекта текстуры вам необходимо указать позицию (мы используем GL_TEXTURE_2D) и имя файла. После вы можете вызвать функцию Load(). Она может вернуть код ошибки если, например, файл не существует, или если ImageMagick получит другие виды ошибок. encountered any other error. Если вы хотите использовать конкретный экземпляр текстуры, вы должны привязать его к одному из текстурных модулей.

Вот так мы используем ImageMagick для загрузки из файла и подготовки памяти для загрузки в OpenGL. Мы начинаем с инициализации свойства класса типа Magic::Image используя имя файла текстуры. Этот вызов загружает текстуру в память, которая задана private и не может быть напрямую использована OpenGL. Затем мы записываем изображение в объект Magick::Blob используя формат RGBA (красный, зеленый, синий и альфа канал). BLOB (большой бинарный объект) — это полезный механизм для хранения зашифрованного изображения в память так, что оно может быть использовано сторонними программами. Если будут какие-либо ошибки, то будет брошено исключение, поэтому мы должны быть готовы для него.

Эта функция OpenGL очень похожа на glGenBuffers(), с которой мы уже хорошо знакомы. Она генерирует указанное число объектов текстур и помещает их в указатель на массив GLuint (второй параметр). В нашем случае нам потребуется только 1 объект.

Мы собираемся сделать несколько вызовов, связанных с текстурой, и в похожей на буфер вершин манере, OpenGL должен знать, с каким объектом текстур работать. Эта цель функции glBindTexture(). Она сообщает OpenGL объект текстуры, который относится ко всем вызовам, связанным с текстурами, до тех пор, пока новый объект текстур не будет передан. В дополнении к указателю (второй параметр) мы также указываем позицию текстуры, которая может принимать значения GL_TEXTURE_1D, GL_TEXTURE_2D и т.д. Вполне можно использовать различные типы объектов текстур для каждой из позиций одновременно. В нашем случае позиция — это часть конструктора (сейчас мы используем GL_TEXTURE_2D).

Гораздо более сложная функция для загрузки главной части объекта текстуры, что по сути, сами данные текстуры. Существует несколько функций glTexImage*, доступных для каждой позиции текстуры. Позиция всегда первый параметр. Второй — это LOD или уровень детализации (Level-Of-Detail). Объект текстуры может хранить одну и ту же текстуру в различном разрешении, понятие, известное как mip-отображение (mip — «много в одном»). Каждое mip-отображение имеет различный коэффициент LOD, 0 для максимального качества, и с увеличением качество падает. Пока что мы имеем только 1 mip-отображение, поэтому мы передаем 0.

Следующий параметр — внутренний формат, в котором OpenGL хранит текстуру. Для примера, вы можете передать текстуру со всеми 4 каналами (красный, зеленый, голубой и альфа), но если вы укажете GL_RED, то вы получите текстуру только с красным каналом, что выглядит довольно … красно (попробуйте это!). Мы используем GL_RGBA для получения всех цветов текстуры. Следующие 2 параметра ширина и высота текстуры в текселях. ImageMagick сохраняет эту информацию для нас когда загружает изображение, и мы получаем эти данные через функции Image::columns()/rows(). Пятый параметр — рамка, которую мы оставим равной 0.

Последние 3 параметра указывают источник входящих данных текстуры. Это формат, тип и адрес в памяти. Формат указывает количество каналов, которые должны соответствовать значению из BLOB. Тип определяет вид данных относительно каждого канала. OpenGL поддерживает множество типов данных, но в ImageMagick BLOB имеет только 1 байт на канал, поэтому мы используем GL_UNSIGNED_BYTE. Наконец, мы указываем адрес данных, которые извлекаются из BLOB’а через функцию Blob::data().

Широкая функция glTexParameterf управляет многими аспектами операции выборки текстур. Эти аспекты — часть состояния сэмплера текстуры. Здесь мы указываем фильтры, которые будут использованы для увеличения и минимализации. Каждая текстура имеет заданные ширину и высоту, но очень редко они совпадают с пропорциями треугольника. В большинстве случаев треугольник больше или меньше чем текстура. В этом случае тип фильтра определяет как именно увеличить или уменьшить текстуру для совпадения пропорций. Если треугольник, проходящий растеризацию, больше чем текстура (например очень близок к камере), то у нас некоторые пиксели будут использовать один текстел. А если меньше (очень далеко от камеры) сразу несколько текселей используются для одного пикселя. Мы выбрали фильтр линейной интерполяции для обоих случаев. Как мы уже видели ранее, линейная интерполяция дает хороший результат путем смешивания цвета 2×2 текселя основываясь на текущей позиции текселя (вычисляется путем масштабирования координат текстуры ее размерами).

Так как наше 3D приложение постоянно разрастается, мы возможно захотим использовать множество различных текстур во множестве вызовов отрисовки в функции рендера. Прежде чем делать любой вызов мы должны привязать объект текстур, а так же разрешить использование конкретного модуля текстур, что бы она была доступна в фрагментном шейдере. Эта функция принимает модуль текстуры как параметр типа enum (GL_TEXTURE0, GL_TEXTURE1 и т.д.). Тем самым он станет активным через glActiveTexture() и затем привязываем объект текстур к модулю. Связь будет до тех пор, пока для этого модуля не будет вызвана Texture::Bind() для другой текстуры.

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

А это новый фрагментный шейдер. У него входящая переменная, названная TexCoord0, которая содержит интерполированые координаты текстуры, полученные из вершинного шейдера. Так же у нас новая uniform-переменная, названная gSampler, типа sampler2D. Это пример сэмплера uniform-переменной. Приложению требуется задать значение модуля текстуры в эту переменную что бы фрагментный шейдер имел доступ к текстуре. Функция main делает только одну вещь — она вызывает внутреннюю функцию texture2D что бы использовать текстуру. Первый параметр это сэмплер uniform-переменной и второй — координаты текстуры. Возращенное значение — это сэмплер текселя (который, в нашем случае, содержит только цвет), который уже прошел фильтрацию. Это итоговый цвет пикселя в данном уроке. В последующих мы уроках мы увидим, что свет просто влияет на цвет полагаясь на параметры света.

До этого момента наш вершинный буфер состоял из последовательного списка экземпляров структуры Vector3f, которая содержала только позицию. Теперь у нас есть структура ‘Vertex’, содержащая так же и координаты текстуры в формате Vector2f.

Цикл рендера достаточно изменился. Мы начинаем с разрешения использования атрибутов вершин 1 для координат текстур в дополнении к атрибуту 0, который уже занят для позиции. Это соответствует их расположению в вершинном шейдере. Затем мы вызываем glVertexAttribPointer для указания позиции координат текстуры в вершинном буфере. Они представлены в виде 2 вещественных числах, что и указано во 2 и 3 параметрах. Обратите внимание на 4 параметр. Это размер структуры вершины, и он указывается и для вектора позиции и вектора координат. Этот параметр еще называют как «расстояние между вершинами» (vertex stride), он говорит OpenGL количество байтов между началом атрибутов одной вершины и началом уже следующей. В нашем случае буфер содержит: pos0, texture coords0, pos1, texture coords1 и т.д. В предыдущих уроках у нас была только позиция, так что мы могли установить 0 или sizeof(Vector3f). Сейчас же мы имеем больше чем один атрибут, поэтому размер обязательно должен быть равен размеру структуры. Последний параметр — смещение в байтах от начала структуры до атрибутов текстуры. Мы преобразовываем в GLvoid* потому, что функция ожидает смещение в таком формате.

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

Эти вызовы OpenGL не обязательны для текстурирования, но я их добавил для того, что бы картинка была лучше (попробуйте их отключить…). Они включают отброс задней поверхности для дополнительной оптимизации, и используется что бы отбраковывать треугольники до затратных процессов растеризации. Обосновывается это тем, что 50% поверхностей объектов скрыты от нас (задняя сторона человека, дома, автомобиля и т.д.). Функция glFrontFace() говорит OpenGL, что вершины в треугольнике подаются в сторону движения часовой стрелки. То есть, если вы смотрите прямо на плоскость треугольника, то вы заметите, что вершины указаны в часовом порядке. glCullFace() сообщает GPU, что бы он отбрасывал обратные стороны треугольника. Это значит, что «внутри» объекта ничего рендериться не будет, только внешняя часть. Наконец, включаем отбрасывание задних сторон (по умолчанию выключено).

Здесь мы устанавливаем индексы модулей текстуры, который мы собираемся использовать внутри сэмплера uniform-переменной в шейдере. ‘gSampler’ это переменная, значение которой было задано ранее через glGetUniformLocation(). Важно запомнить, что индекс модуля текстуры, который использован здесь, не enum OpenGL’я GL_TEXTURE0 (который имеет другое значение).

Здесь мы создаем объект Текстуры и загружаем его. ‘test.png’ добавлен к исходникам этого урока, но ImageMagick должна суметь обработать любой файл, переданный ей.

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

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

Добро пожаловать на наш пятый урок. В этом уроке вы узнаете:

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

UV-координаты

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

Каждая вершина помимо позиции имеет несколько дополнительных полей, а также U и V. Эти координаты используются применительно к текстуре, как показано на рисунке:

Обратите внимание, как текстура искажается на треугольнике.

Загрузка Bitmap-изображений

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

Объявляем функцию для загрузки изображений:

Вызываться она будет так:

Теперь перейдем непосредственно к чтению файла.

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

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

Заголовок всегда начинается с букв BM. Вы можете открыть файл в HEX-редакторе и убедиться в этом самостоятельно, а можете посмотреть на наш скриншот:

Итак, мы проверяем первые два байта и если они не являются буквами “BM”, то файл не является BMP-файлом или испорчен:

Теперь мы читаем размер изображения, смещение данных изображения в файле и т. п.:

Проверим и исправим полученные значения:

Теперь, так как мы знаем размер изображения, то можем выделить область памяти, в которую поместим данные:

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

  • Создайте текстуру
  • Привяжите ее
  • Заполните
  • Сконфигурируйте

GL_RGB в glTextImage2D указывает на то, что мы работает с 3х компонентным цветом. А GL_BGR указывает на то, как данные представлены в памяти. На самом деле в BMP-файлах цветовые данные хранятся не в RGB, а в BGR (если быть точным, то это связано с тем, как хранятся числа в памяти), поэтому необходимо сообщить об этом OpenGL:

Последние две строки мы поясним позднее, а пока в части C++ мы должны использовать нашу функцию для загрузки текстуры:

**Очень важное замечание: **используйте текстуры с шириной и высотой степени двойки! То есть:

  • Хорошие: 128128, 256256, 10241024, 2*2…
  • Плохие: 127128, 35, …
  • Приемлемые: 128*256

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

Что же, давайте посмотрим на наш Фрагментный шейдер:

  • Фрагментному шейдеру требуются UV-координаты. Это понятно.
  • Также, ему необходим “sampler2D”, чтобы знать, с какой текстурой работать (вы можете получить доступ к нескольким текстурам в одном шейдере т. н. мультитекстурирование)
  • И наконец, доступ к текстуре завершается вызовом texture(), который возвращает vec4 (R, G, B, A). A-компоненту мы разберем немного позднее.

Вершинный шейдер также прост. Все, что мы делаем — это передаем полученные UV-координаты в фрагментный шейдер:

Помните “layout(location = 1) in vec3 vertexColor” из Урока 4? Здесь мы делаем абсолютно тоже самое, только вместо передачи буфера с цветом каждой вершины мы будем передавать буфер с UV-координатами каждой вершины:

Указанные UV-координаты относятся к такой модели:

Остальное очевидно. Мы создаем буфер, привязываем его, заполняем, настраиваем и выводим Буфер Вершин как обычно. Только будьте осторожны, так как в glVertexAttribPointer для буфера текстурных координат второй параметр (размер) будет не 3, а 2.

И вот такой результат мы получим:

в увеличенном варианте:

Фильтрация и мип-маппинг.

Как вы можете видеть на скриншоте выше, качество текстуры не очень хорошее. Это потому, что в нашей процедуре загрузки BMP-изображения (loadBMP_custom) мы указали:

Это означает, что в нашем фрагментном шейдере, texture() возвращает строго тексель, который находится по указанным текстурным координатам:

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

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

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

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

И линейная, и анизотропная фильтрация имеют недостаток. Если текстура просматривается с большого расстояния, то смешивать 4 текселя будет недостаточно. То есть, если ваша 3D модель находится так далеко, что занимает на экране всего 1 фрагмент, то фильный цвет фрагмента будет являться средним всех текселей текстуры. Естественно, это не реализовано из-за соображений производительности. Для этой цели существует так называемый мип-маппинг:

  • При инициализации вы уменьшаете масштаб текстуры до тех пор, пока не получите изображение 1х1 (которое по сути будет являться средним значением всех текселей текстуры)
  • Когда вы выводите объект, то вы выбираете тот мип-мап, который наиболее приемлем в данной ситуации.
  • Вы применяете к этому мип-мапу фильтрацию
  • А для большего качества вы можете использовать 2 мип-мапа и смешать результат.

К счастью для нас, все это делается очень просто с помощью OpenGL:

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

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

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

На этом шаге вы наверное хотите узнать, как же все-таки загружать JPEG файлы вместо TGA?

Короткий ответ: даже не думайте об этом. Есть идея получше.

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

  • Скачайте The Compressonator, утилита от ATI
  • Загрузите в нее текстуру, размер которой является степенью двойки
  • Сожмите ее в DXT1, DXT3 или в DXT5 (о разнице между форматами можете почитать на Wikipedia)

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

После этих шагов вы имеете сжатое изображение, которое прямо совместимо с GPU. И когда вы вызовите texture() в шейдере, то текстура будет распакована на лету. Это может показаться более медленным, однако это требует гораздо меньше памяти, а значит пересылаемых данных будет меньше. Пересылка данных всегда будет дорогой операцией, в то время как декомпрессия является практически бесплатной. Как правило, использование сжатия текстур повышает быстродействие на 20%.

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

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

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

Сделано. Так как мы можем использовать 3 разных формата (DXT1, DXT3, DXT5), то необходимо в зависимости от флага “fourCC”, сказать OpenGL о формате данных.

Создание текстуры выполняется как обычно:

Следующим шагом мы загружаем мип-мапы:

DXT компрессия пришла к нам из DirectX, где координатная текстура V является инвертированной по сравнению с OpenGL. Поэтому, если вы используете сжатые текстуры, то вам необходимо использовать (coord.u, 1.0 — coord.v), чтобы исправить тексель. Вы можете выполнять это как при экспорте текстуры, так и в загрузчике или в шейдере.

Заключение

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

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

OpenGL: How do I apply a texture to this cube? [duplicate]

I’m trying to learn OpenGL, and I’ve been going through a lot of tutorials on loading a texture, but every single one seems to miss the most important step: how do I actually put a texture on something?

I’m using Python for this, and here is my function that loads the texture:

And here is the function that loads my Cube:

And here’s the main loop:

But the cube is untextured. I don’t know how to actually use the loaded texture on the cube, and every texture tutorial I find takes me as far as the loadTexture function without actually telling me how to use it. Where do I call it? What do I do with texid?

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

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