Эта статья является продолжением серии статей о WebGL. Первая началась с основ, а предыдущая была об анимации.
Как мы применяем текстуры в WebGL? Вы, вероятно, могли бы вывести, как это сделать, читая статьи об обработке изображений, но, вероятно, будет легче понять, если мы рассмотрим это более подробно.
Первое, что нам нужно сделать, это настроить наши шейдеры для использования текстур. Вот изменения в вершинном шейдере. Нам нужно передать координаты текстуры. В этом случае мы просто передаем их прямо в фрагментный шейдер.
#version 300 es
in vec4 a_position;
*in vec2 a_texcoord;
uniform mat4 u_matrix;
+// varying для передачи координат текстуры в фрагментный шейдер
+out vec2 v_texcoord;
void main() {
// Умножаем позицию на матрицу.
gl_Position = u_matrix * a_position;
+ // Передаем texcoord в фрагментный шейдер.
+ v_texcoord = a_texcoord;
}
В фрагментном шейдере мы объявляем uniform sampler2D, который позволяет нам ссылаться
на текстуру. Мы используем координаты текстуры, переданные из вершинного шейдера,
и мы вызываем texture
, чтобы найти цвет из этой текстуры.
#version 300 es
precision highp float;
// Передается из вершинного шейдера.
*in vec2 v_texcoord;
*// Текстура.
*uniform sampler2D u_texture;
out vec4 outColor;
void main() {
* outColor = texture(u_texture, v_texcoord);
}
Нам нужно настроить координаты текстуры
// ищем, куда должны идти данные вершин.
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
*var texcoordAttributeLocation = gl.getAttribLocation(program, "a_texcoord");
...
*// создаем буфер texcoord, делаем его текущим ARRAY_BUFFER
*// и копируем значения texcoord
*var texcoordBuffer = gl.createBuffer();
*gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
*setTexcoords(gl);
*
*// Включаем атрибут
*gl.enableVertexAttribArray(texcoordAttributeLocation);
*
*// Говорим атрибуту, как получать данные из texcoordBuffer (ARRAY_BUFFER)
*var size = 2; // 2 компонента на итерацию
*var type = gl.FLOAT; // данные - 32-битные значения с плавающей точкой
*var normalize = true; // конвертируем из 0-255 в 0.0-1.0
*var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) каждую итерацию, чтобы получить следующий texcoord
*var offset = 0; // начинаем с начала буфера
*gl.vertexAttribPointer(
* texcoordAttributeLocation, size, type, normalize, stride, offset);
И вы можете видеть координаты, которые мы используем, которые отображают всю текстуру на каждый квадрат нашей ‘F’.
*// Заполняем буфер координатами текстуры для F.
*function setTexcoords(gl) {
* gl.bufferData(
* gl.ARRAY_BUFFER,
* new Float32Array([
* // левая колонка спереди
* 0, 0,
* 0, 1,
* 1, 0,
* 0, 1,
* 1, 1,
* 1, 0,
*
* // верхняя перекладина спереди
* 0, 0,
* 0, 1,
* 1, 0,
* 0, 1,
* 1, 1,
* 1, 0,
* ...
* ]),
* gl.STATIC_DRAW);
Нам также нужна текстура. Мы могли бы создать одну с нуля, но в этом случае давайте загрузим изображение, поскольку это, вероятно, самый распространенный способ.
Вот изображение, которое мы собираемся использовать
Какое захватывающее изображение! На самом деле изображение с ‘F’ на нем имеет четкое направление, поэтому легко сказать, повернуто оно или перевернуто и т.д., когда мы используем его как текстуру.
Дело в загрузке изображения в том, что это происходит асинхронно. Мы запрашиваем изображение для загрузки, но браузеру требуется время, чтобы скачать его. Есть обычно 2 решения для этого. Мы могли бы заставить код ждать, пока текстура не скачается, и только тогда начать рисовать. Другое решение - создать какую-то текстуру для использования, пока изображение скачивается. Таким образом, мы можем начать рендеринг немедленно. Затем, как только изображение было скачано, мы копируем изображение в текстуру. Мы будем использовать этот метод ниже.
*// Создаем текстуру.
*var texture = gl.createTexture();
*gl.bindTexture(gl.TEXTURE_2D, texture);
*
*// Заполняем текстуру 1x1 синим пикселем.
*gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
* new Uint8Array([0, 0, 255, 255]));
*
*// Асинхронно загружаем изображение
*var image = new Image();
*image.src = "resources/f-texture.png";
*image.addEventListener('load', function() {
* // Теперь, когда изображение загружено, копируем его в текстуру.
* gl.bindTexture(gl.TEXTURE_2D, texture);
* gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image);
* gl.generateMipmap(gl.TEXTURE_2D);
*});
И вот это
Что, если бы мы хотели использовать только часть текстуры на передней части ‘F’? Текстуры ссылаются с “координатами текстуры”, и координаты текстуры идут от 0.0 до 1.0 слева направо по текстуре и от 0.0 до 1.0 от первого пикселя на первой строке до последнего пикселя на последней строке. Обратите внимание, я не сказал верх или низ. Верх и низ не имеют смысла в пространстве текстуры, потому что пока вы не нарисуете что-то и не сориентируете это, нет верха и низа. Важно то, что вы предоставляете данные текстуры в WebGL. Начало этих данных начинается с координаты текстуры 0,0, и конец этих данных находится в 1,1
Я загрузил текстуру в photoshop и посмотрел различные координаты в пикселях.
Чтобы конвертировать из координат пикселей в координаты текстуры, мы можем просто использовать
texcoordX = pixelCoordX / (width - 1)
texcoordY = pixelCoordY / (height - 1)
Вот координаты текстуры для передней части.
// левая колонка спереди
38 / 255, 44 / 255,
38 / 255, 223 / 255,
113 / 255, 44 / 255,
38 / 255, 223 / 255,
113 / 255, 223 / 255,
113 / 255, 44 / 255,
// верхняя перекладина спереди
113 / 255, 44 / 255,
113 / 255, 85 / 255,
218 / 255, 44 / 255,
113 / 255, 85 / 255,
218 / 255, 85 / 255,
218 / 255, 44 / 255,
// средняя перекладина спереди
113 / 255, 112 / 255,
113 / 255, 151 / 255,
203 / 255, 112 / 255,
113 / 255, 151 / 255,
203 / 255, 151 / 255,
203 / 255, 112 / 255,
Я также использовал похожие координаты текстуры для задней части. И вот это.
Не очень захватывающий дисплей, но, надеюсь, он демонстрирует, как использовать координаты текстуры. Если вы создаете геометрию в коде (кубы, сферы и т.д.), обычно довольно легко вычислить любые координаты текстуры, которые вы хотите. С другой стороны, если вы получаете 3D модели из программ 3D моделирования, таких как Blender, Maya, 3D Studio Max, то ваши художники (или вы) будут настраивать координаты текстуры в этих пакетах, используя UV редактор.
Так что происходит, если мы используем координаты текстуры вне диапазона 0.0 до 1.0. По умолчанию WebGL повторяет текстуру. 0.0 до 1.0 - это одна ‘копия’ текстуры. 1.0 до 2.0 - это другая копия. Даже -4.0 до -3.0 - это еще одна копия. Давайте отобразим плоскость, используя эти координаты текстуры.
-3, -1,
2, -1,
-3, 4,
-3, 4,
2, -1,
2, 4,
и вот это
Вы можете сказать WebGL не повторять текстуру в определенном направлении, используя CLAMP_TO_EDGE
. Например
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
вы также можете сказать WebGL отражать текстуру, когда она повторяется, используя gl.MIRRORED_REPEAT
.
Нажмите кнопки в примере выше, чтобы увидеть разницу.
Вы, возможно, заметили вызов gl.generateMipmap
еще когда мы загружали текстуру. Для чего это?
Представьте, у нас была эта текстура 16x16 пикселей.
Теперь представьте, мы попытались нарисовать эту текстуру на полигоне размером 2x2 пикселя на экране. Какие цвета мы должны сделать для этих 4 пикселей? Есть 256 пикселей на выбор. В Photoshop, если бы вы масштабировали изображение 16x16 пикселей до 2x2, он бы усреднил 8x8 пикселей в каждом углу, чтобы сделать 4 пикселя в изображении 2x2. К сожалению, чтение 64 пикселей и усреднение их всех вместе было бы слишком медленно для GPU. На самом деле представьте, если бы у вас была текстура 2048x2084 пикселей, и вы попытались нарисовать ее 2x2 пикселя. Чтобы сделать то, что делает Photoshop для каждого из 4 пикселей в результате 2x2, ему пришлось бы усреднить 1024x1024 пикселя или 1 миллион пикселей, умноженный на 4. Это слишком много, чтобы делать и все еще быть быстрым.
Так что GPU использует мипмап. Мипмап - это коллекция прогрессивно меньших изображений, каждое из которых в 1/4 размера предыдущего. Мипмап для текстуры 16x16 выше выглядел бы примерно так.
Обычно каждый меньший уровень - это просто билинейная интерполяция предыдущего уровня, и это
то, что делает gl.generateMipmap
. Он смотрит на самый большой уровень и генерирует все меньшие уровни для вас.
Конечно, вы можете предоставить меньшие уровни сами, если хотите.
Теперь, если вы попытаетесь нарисовать эту текстуру 16x16 пикселей только 2x2 пикселя на экране, WebGL может выбрать мип, который 2x2, который уже был усреднен из предыдущих мипов.
Вы можете выбрать, что делает WebGL, установив фильтрацию текстуры для каждой текстуры. Есть 6 режимов
NEAREST
= выбрать 1 пиксель из самого большого мипаLINEAR
= выбрать 4 пикселя из самого большого мипа и смешать ихNEAREST_MIPMAP_NEAREST
= выбрать лучший мип, затем выбрать один пиксель из этого мипаLINEAR_MIPMAP_NEAREST
= выбрать лучший мип, затем смешать 4 пикселя из этого мипаNEAREST_MIPMAP_LINEAR
= выбрать лучшие 2 мипа, выбрать 1 пиксель из каждого, смешать ихLINEAR_MIPMAP_LINEAR
= выбрать лучшие 2 мипа, выбрать 4 пикселя из каждого, смешать ихВы можете увидеть важность мипов в этих 2 примерах. Первый показывает, что если вы используете NEAREST
или LINEAR
и выбираете только из самого большого изображения, то вы получите много мерцания, потому что когда вещи
двигаются, для каждого пикселя, который он рисует, ему приходится выбирать один пиксель из самого большого изображения. Это меняется в зависимости
от размера и позиции, и поэтому иногда он выберет один пиксель, в другое время другой, и поэтому он
мерцает.
Пример выше преувеличен, чтобы показать проблему.
Обратите внимание, как сильно мерцают те, что слева и в середине, тогда как те, что справа, мерцают меньше.
Те, что справа, также имеют смешанные цвета, поскольку они используют мипы. Чем меньше вы рисуете текстуру, тем дальше друг от друга WebGL будет
выбирать пиксели. Вот почему, например, тот, что внизу посередине, даже несмотря на то, что он использует LINEAR
и смешивает
4 пикселя, мерцает разными цветами, потому что эти 4 пикселя из разных углов изображения 16x16 в зависимости от того, какие
4 выбраны, вы получите другой цвет. Тот, что внизу справа, хотя остается постоянного цвета,
потому что он использует второй по величине мип.
Этот второй пример показывает полигоны, которые уходят глубоко вдаль.
6 лучей, идущих в экран, используют 6 режимов фильтрации, перечисленных выше. Луч вверху слева использует NEAREST
,
и вы можете видеть, что он явно очень блочный. Тот, что вверху посередине, использует LINEAR
, и он не намного лучше.
Тот, что вверху справа, использует NEAREST_MIPMAP_NEAREST
. Нажмите на изображение, чтобы переключиться на текстуру, где каждый мип
разного цвета, и вы легко увидите, где он выбирает использовать конкретный мип. Тот, что внизу слева, использует
LINEAR_MIPMAP_NEAREST
, что означает, что он выбирает лучший мип, а затем смешивает 4 пикселя в этом мипе. Вы все еще можете видеть
четкую область, где он переключается с одного мипа на следующий мип. Тот, что внизу посередине, использует NEAREST_MIPMAP_LINEAR
,
что означает выбор лучших 2 мипов, выбор одного пикселя из каждого и смешивание
их. Если вы посмотрите внимательно, вы можете увидеть, как он все еще блочный, особенно в горизонтальном направлении.
Тот, что внизу справа, использует LINEAR_MIPMAP_LINEAR
, который выбирает лучшие 2 мипа, выбирает 4 пикселя из каждого,
и смешивает все 8 пикселей.
Вы можете думать, зачем вам когда-либо выбирать что-то другое, кроме LINEAR_MIPMAP_LINEAR
, который, возможно,
лучший. Есть много причин. Одна в том, что LINEAR_MIPMAP_LINEAR
самый медленный. Чтение 8 пикселей
медленнее, чем чтение 1 пикселя. На современном GPU оборудовании это, вероятно, не проблема, если вы используете только 1
текстуру за раз, но современные игры могут использовать 2-4 текстуры одновременно. 4 текстуры * 8 пикселей на текстуру =
необходимость читать 32 пикселя для каждого нарисованного пикселя. Это будет медленно. Другая причина в том, что если вы пытаетесь
достичь определенного эффекта. Например, если вы хотите, чтобы что-то имело этот пикселизированный ретро вид, возможно, вы
хотите использовать NEAREST
. Мипы также занимают память. На самом деле они занимают на 33% больше памяти. Это может быть много памяти,
особенно для очень большой текстуры, как вы могли бы использовать на титульном экране игры. Если вы никогда не собираетесь
рисовать что-то меньше, чем самый большой мип, зачем тратить память на меньшие мипы. Вместо этого просто используйте NEAREST
или LINEAR
, поскольку они используют только первый мип.
Чтобы установить фильтрацию, вы вызываете gl.texParameter
так
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
TEXTURE_MIN_FILTER
- это настройка, используемая, когда размер, который вы рисуете, меньше, чем самый большой мип.
TEXTURE_MAG_FILTER
- это настройка, используемая, когда размер, который вы рисуете, больше, чем самый большой мип. Для
TEXTURE_MAG_FILTER
только NEAREST
и LINEAR
являются валидными настройками.
Что нужно знать, WebGL2 требует, чтобы текстуры были “texture complete”, иначе они не будут рендериться. “texture complete” означает, что либо
Вы установили фильтрацию так, чтобы она использовала только первый уровень мипа, что означает
установку TEXTURE_MIN_FILTER
либо в LINEAR
, либо в NEAREST
.
Если вы используете мипы, то они должны быть правильных размеров, и вы должны предоставить ВСЕ ИЗ НИХ вплоть до размера 1x1.
Самый простой способ сделать это - вызвать gl.generateMipmap
. В противном случае, если вы предоставляете свои собственные мипы, вам нужно предоставить
все из них, или вы получите ошибку.
Общий вопрос: “Как я могу применить другое изображение к каждой грани куба?”. Например, скажем, у нас были эти 6 изображений.
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
3 ответа приходят на ум
сделать сложный шейдер, который ссылается на 6 текстур, и передать какую-то дополнительную информацию на вершину в вершинный шейдер, которая передается в фрагментный шейдер, чтобы решить, какую текстуру использовать. НЕ ДЕЛАЙТЕ ЭТОГО! Немного размышлений сделало бы ясным, что вам пришлось бы написать тонны разных шейдеров, если бы вы хотели сделать то же самое для разных форм с большим количеством сторон и т.д.
нарисовать 6 плоскостей вместо куба. Это общее решение. Это не плохо, но это также работает только для маленьких форм, как куб. Если бы у вас была сфера с 1000 квадратов, и вы хотели положить другую текстуру на каждый квадрат, вам пришлось бы нарисовать 1000 плоскостей, и это было бы медленно.
Решение, осмелюсь сказать, лучшее - это положить все изображения в 1 текстуру и использовать координаты текстуры, чтобы отобразить другую часть текстуры на каждую грань куба. Это техника, которую используют практически все высокопроизводительные приложения (читай игры). Так, например, мы бы положили все изображения в одну текстуру, возможно, так
и затем использовать другой набор координат текстуры для каждой грани куба.
// выбираем изображение вверху слева
0 , 0 ,
0 , 0.5,
0.25, 0 ,
0 , 0.5,
0.25, 0.5,
0.25, 0 ,
// выбираем изображение вверху посередине
0.25, 0 ,
0.5 , 0 ,
0.25, 0.5,
0.25, 0.5,
0.5 , 0 ,
0.5 , 0.5,
// выбираем изображение вверху справа
0.5 , 0 ,
0.5 , 0.5,
0.75, 0 ,
0.5 , 0.5,
0.75, 0.5,
0.75, 0 ,
// выбираем изображение внизу слева
0 , 0.5,
0.25, 0.5,
0 , 1 ,
0 , 1 ,
0.25, 0.5,
0.25, 1 ,
// выбираем изображение внизу посередине
0.25, 0.5,
0.25, 1 ,
0.5 , 0.5,
0.25, 1 ,
0.5 , 1 ,
0.5 , 0.5,
// выбираем изображение внизу справа
0.5 , 0.5,
0.75, 0.5,
0.5 , 1 ,
0.5 , 1 ,
0.75, 0.5,
0.75, 1 ,
И мы получаем
Этот стиль применения нескольких изображений, используя 1 текстуру, часто называется texture atlas. Это лучше всего, потому что есть только 1 текстура для загрузки, шейдер остается простым, поскольку ему нужно ссылаться только на 1 текстуру, и это требует только 1 вызов отрисовки для рисования формы вместо 1 вызова отрисовки на текстуру, как это могло бы быть, если бы мы разделили это на плоскости.
Несколько других очень важных вещей, которые вы, возможно, захотите знать о текстурах. Одна - как работает состояние текстурного блока. Одна - как использовать 2 или более текстур одновременно. Другая
Далее давайте начнем упрощать с меньшим количеством кода, больше веселья.