Using textures in WebGL
Сейчас наша программа рисует вращающийся объёмный куб - давайте натянем на него текстуру вместо заливки граней одним цветом.
Загрузка текстур
Сначала нужно добавить код для загрузки текстур. В нашем случае мы будем использовать одну текстуру, натянутую на все шесть граней вращающегося куба, но этот подход может быть использован для загрузки любого количества текстур.
Примечание: Важно помнить, что загрузка текстур следует правилам кросс-доменности, что означает, что вы можете загружать текстуры только с сайтов, для которых ваш контент является 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()
. Сначала функция создаёт текстуру из единственного голубого пикселя, используя texImage2D()
. Таким образом текстура может быть использована сразу (как сплошной голубой цвет) при том, что загрузка изображения может занять некоторое время.
Чтобы загрузить текстуру из файла изображения, функция создаёт объект Image
и присваивает атрибуту src
адрес, с которого мы хотим загрузить текстуру. Функция, которую мы назначили на событие image.onload
,будет вызвана после завершения загрузки изображения. В этот момент мы вызываем texImage2D()
, используя загруженное изображение как исходник для текстуры. Затем мы устанавливаем фильтрацию и натяжение, исходя из того, является ли размер изображения степенью 2 или нет.
В WebGL1 изображения размера, не являющегося степенью 2, могут использовать только NEAREST
или LINEAR
фильтрацию, и для них нельзя создать mipmap. Также для таких изображений мы должны установить натяжение CLAMP_TO_EDGE
. С другой стороны, если изображение имеет размер степени 2 по обеим осям, WebGL может производить более качественную фильтрацию, использовать mipmap и режимы натяжения REPEAT
или MIRRORED_REPEAT
.
Примером повторяющейся текстуры является изображение нескольких кирпичей, которое размножается для покрытия поверхности и создания изображения кирпичной стены.
Мипмаппинг и UV-повторение могут быть отключены с помощью texParameteri()
. Так вы сможете использовать текстуры с размером, не являющимся степенью 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.