Using textures in WebGL
Сейчас наша программа рисует вращающийся объёмный куб - давайте натянем на него текстуру вместо заливки граней одним цветом.
Загрузка текстур
Сначала нужно добавить код для загрузки текстур. В нашем случае мы будем использовать одну текстуру, натянутую на все шесть граней вращающегося куба, но этот подход может быть использован для загрузки любого количества текстур.
Примечание: Важно помнить, что загрузка текстур следует правилам кросс-доменности (en-US), что означает, что вы можете загружать текстуры только с сайтов, для которых ваш контент является CORS доверенным. См. подробности в секции "Кросс-доменные текстуры" ниже.
Код для загрузки текстур выглядит так::
//
// Инициализация текстуры и загрузка изображения.
// Когда загрузка изображения завершена - копируем его в текстуру.
//
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Так как изображение будет загружено из интернета,
// может потребоваться время для полной загрузки.
// Поэтому сначала мы помещаем в текстуру единственный пиксель, чтобы
// её можно было использовать сразу. После завершения загрузки
// изображения мы обновим текстуру.
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // непрозрачный синий
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
width, height, border, srcFormat, srcType,
pixel);
const image = new Image();
image.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
srcFormat, srcType, image);
// У WebGL1 иные требования к изображениям, имеющим размер степени 2,
// и к не имеющим размер степени 2, поэтому проверяем, что изображение
// имеет размер степени 2 в обеих измерениях.
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
// Размер соответствует степени 2. Создаём MIP'ы.
gl.generateMipmap(gl.TEXTURE_2D);
} else {
// Размер не соответствует степени 2.
// Отключаем MIP'ы и устанавливаем натяжение по краям
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
};
image.src = url;
return texture;
}
function isPowerOf2(value) {
return (value & (value - 1)) == 0;
}
Функция loadTexture() начинается с создания объекта WebGL texture вызовом функции createTexture() (en-US). Сначала функция создаёт текстуру из единственного голубого пикселя, используя texImage2D() (en-US). Таким образом текстура может быть использована сразу (как сплошной голубой цвет) при том, что загрузка изображения может занять некоторое время.
Чтобы загрузить текстуру из файла изображения, функция создаёт объект Image и присваивает атрибуту src адрес, с которого мы хотим загрузить текстуру. Функция, которую мы назначили на событие image.onload,будет вызвана после завершения загрузки изображения. В этот момент мы вызываем texImage2D() (en-US), используя загруженное изображение как исходник для текстуры. Затем мы устанавливаем фильтрацию и натяжение, исходя из того, является ли размер изображения степенью 2 или нет.
В WebGL1 изображения размера, не являющегося степенью 2, могут использовать только NEAREST или LINEAR фильтрацию, и для них нельзя создать mipmap. Также для таких изображений мы должны установить натяжение CLAMP_TO_EDGE. С другой стороны, если изображение имеет размер степени 2 по обеим осям, WebGL может производить более качественную фильтрацию, использовать mipmap и режимы натяжения REPEAT или MIRRORED_REPEAT.
Примером повторяющейся текстуры является изображение нескольких кирпичей, которое размножается для покрытия поверхности и создания изображения кирпичной стены.
Мипмаппинг и UV-повторение могут быть отключены с помощью texParameteri() (en-US). Так вы сможете использовать текстуры с размером, не являющимся степенью 2 (NPOT - non-power-of-two), ценой отключения мипмаппинга, UV-натяжения, UV-повторения, и вам самому придётся контролировать, как именно устройство будет обрабатывать текстуру.
// также разрешено gl.NEAREST вместо gl.LINEAR, но не mipmap. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // Не допускаем повторения по s-координате. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // Не допускаем повторения по t-координате. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
Повторим, что с этими параметрами совместимые WebGL устройства будут допускать использование текстур с любым разрешением (вплоть до максимального). Без подобной настройки WebGL потерпит неудачу при загрузке NPOT-текстур, и вернёт прозрачный чёрный цвет rgba(0,0,0,0).
Для загрузки изображения добавим вызов loadTexture() в функцию main(). Код можно разместить после вызова initBuffers(gl).
// Загрузка текстуры const texture = loadTexture(gl, 'cubetexture.png');
Отображение текстуры на гранях
Сейчас текстура загружена и готова к использованию. Но сначала мы должны установить соответствие между координатами текстуры и гранями нашего куба. Нужно заменить весь предыдущий код, который устанавливал цвета граней в initBuffers().
const textureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
const textureCoordinates = [
// Front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Back
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Top
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Bottom
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Right
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// Left
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
gl.STATIC_DRAW);
...
return {
position: positionBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
Сначала мы создаём WebGL буфер, в котором сохраняем координаты текстуры для каждой грани, затем связываем его с массивом, в который будем записывать значения.
Массив textureCoordinates определяет координаты текстуры, соответствующие каждой вершине каждой грани. Заметьте, что координаты текстуры лежат в промежутке между 0.0 и 1.0. Размерность текстуры нормализуется в пределах между 0.0 и 1.0, независимо от реального размера изображения.
После определения массива координат текстуры, мы копируем его в буфер, и теперь WebGL имеет данные для отрисовки.
Обновление шейдеров
Мы должны обновить шейдерную программу, чтобы она использовала текстуру, а не цвета.
Вершинный шейдер
Заменяем вершинный шейдер, чтобы он получал координаты текстуры вместо цвета.
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoord = aTextureCoord;
}
`;
Ключевое изменение в том, что вместо получения цвета вершины, мы получаем координаты текстуры и передаём их в вершинный шейдер, сообщая положение точки внутри текстуры, которая соответствует вершине.
Фрагментный шейдер
Также нужно обновить фрагментный шейдер:
const fsSource = `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void) {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
`;
Вместо задания цветового значения цвету фрагмента, цвет фрагмента рассчитывается из текселя (пикселя внутри текстуры), основываясь на значении vTextureCoord, которое интерполируется между вершинами (как ранее интерполировалось значение цвета).
Атрибуты и uniform-переменные
Так как мы изменили атрибуты и добавили uniform-переменные, нам нужно получить их расположение
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
},
};
Рисование текстурированного куба
Сделаем несколько простых изменений в функции drawScene().
Во-первых, удаляем код, который определял цветовые буферы, и заменяем его на:
// Указываем WebGL, как извлечь текстурные координаты из буффера
{
const num = 2; // каждая координата состоит из 2 значений
const type = gl.FLOAT; // данные в буфере имеют тип 32 bit float
const normalize = false; // не нормализуем
const stride = 0; // сколько байт между одним набором данных и следующим
const offset = 0; // стартовая позиция в байтах внутри набора данных
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, num, type, normalize, stride, offset);
gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}
Затем добавляем код, который отображает текстуру на гранях, прямо перед отрисовкой:
// Указываем WebGL, что мы используем текстурный регистр 0 gl.activeTexture(gl.TEXTURE0); // Связываем текстуру с регистром 0 gl.bindTexture(gl.TEXTURE_2D, texture); // Указываем шейдеру, что мы связали текстуру с текстурным регистром 0 gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
WebGL имеет минимум 8 текстурных регистров; первый из них gl.TEXTURE0. Мы указываем, что хотим использовать регистр 0. Затем мы вызываем функцию bindTexture(), которая связывает текстуру с TEXTURE_2D регистра 0. Наконец мы сообщаем шейдеру, что для uSampler используется текстурный регистр 0.
В завершение, добавляем аргумент texture в функцию drawScene().
drawScene(gl, programInfo, buffers, texture, deltaTime);
...
function drawScene(gl, programInfo, buffers, texture, deltaTime) {
Сейчас вращающийся куб должен иметь текстуру на гранях.
Посмотреть код примера полностью | Открыть демо в новом окне
Кросс-доменные текстуры
Загрузка кросс-доменных текстур контролируется правилами кросс-доменного доступа. Чтобы загрузить текстуру с другого домена, она должна быть CORS доверенной. См. детали в статье HTTP access control.
В статье на hacks.mozilla.org есть объяснение с примером, как использовать изображения CORS для создания WebGL текстур.
Примечание: Поддержка CORS для текстур WebGL и атрибут crossOrigin для элементов изображений реализованы в Gecko 8.0.
Tainted (только-для-записи) 2D canvas нельзя использовать в качестве текстур WebGL. Например, 2D <canvas> становится "tainted", когда на ней отрисовано кросс-доменное изображение.
Примечание: Поддержка CORS для Canvas 2D drawImage реализована в Gecko 9.0. Это значит, что использование CORS доверенных кросс-доменных изображений больше не делает 2D canvas "tained" (только-для-записи), и вы можете использовать такую 2D canvas как исходник для текстур WebGL.
Примечание: Поддержка CORS для кросс-доменного видео и атрибут crossorigin для HTML-элемента <video> реализованы в Gecko 12.0.