Ранее мы познакомились с окраской трехмерных предметов. Теперь мы можем сделать их красивыми, цветистыми. Однако при имитации объектов реального мира сложно будет подобрать цвета и произвести раскраску таким образом, чтобы объект выглядел так же, как и в реальности. Гораздо проще воспользоваться текстурированием - то есть нанести на поверхность объекта какие-то изображения, которые помогут имитировать реальность.
Для начала рассмотрим процесс текстурирования на примере двухмерного объекта - прямоугольника. Я сразу приведу полный код страницы:
<!DOCTYPE html> <html> <head> <title>3D in WebGL!</title> <meta charset="utf-8" /> </head> <body> <canvas id="canvas3D" width="400" height="300">Ваш браузер не поддерживает элемент canvas</canvas> <script id="shader-fs" type="x-shader/x-fragment"> precision highp float; uniform sampler2D uSampler; varying vec2 vTextureCoords; void main(void) { gl_FragColor = texture2D(uSampler, vTextureCoords); } </script> <script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; varying vec2 vTextureCoords; void main(void) { gl_Position = vec4(aVertexPosition, 1.0); vTextureCoords = vec2(aVertexPosition.x+0.5,aVertexPosition.y+0.5); } </script> <script type="text/javascript"> var gl; var shaderProgram; var vertexBuffer; var indexBuffer; var texture; // переменная для хранения текстуры function initShaders() { var fragmentShader = getShader(gl.FRAGMENT_SHADER, 'shader-fs'); var vertexShader = getShader(gl.VERTEX_SHADER, 'shader-vs'); shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("Не удалось установить шейдеры"); } gl.useProgram(shaderProgram); shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); } // Функция создания шейдера function getShader(type,id) { var source = document.getElementById(id).innerHTML; var shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("Ошибка компиляции шейдера: " + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } function initBuffers() { var vertices =[ -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5 ]; vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); vertexBuffer.itemSize = 3; var indices = [0, 1, 2, 2, 3, 0]; indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); indexBuffer.numberOfItems = indices.length; } function draw() { gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, vertexBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.drawElements(gl.TRIANGLES, indexBuffer.numberOfItems, gl.UNSIGNED_SHORT,0); } function setupWebGL() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT || gl.DEPTH_BUFFER_BIT); gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); } function setTextures(){ texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); var image = new Image(); image.onload = function() { handleTextureLoaded(image, texture); setupWebGL(); draw(); } image.src = "brickwall.png"; shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler"); gl.uniform1i(shaderProgram.samplerUniform, 0); } function handleTextureLoaded(image, texture) { gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); } window.onload=function(){ var canvas = document.getElementById("canvas3D"); try { gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); } catch(e) {} if (!gl) { alert("Ваш браузер не поддерживает WebGL"); } if(gl){ gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; initShaders(); initBuffers(); setTextures(); } } </script> </body> </html>
В данном случае я использовал в качестве текстуры изображение кирпичной стены, поэтому я получил следующий результат:
Теперь разберем узловые моменты. Первым делом нам нужно где-то держать в памяти загруженную структуру. Для этого создается глобальная переменная
var texture;
.
Большая часть программы должна быть вам знакома по предыдущим примерам. Основное отличие от предыдущих примеров - функция setTextures()
.
Эта функция затем будет вызываться в главной функции вместо метода отрисовки draw()
. Поэтому остановимся на функции setTextures().
Для начала мы создаем текстуру: texture = gl.createTexture();
Для установки изображения в качестве текстуры нам нужен элемент img
. Изображение, установленное для данного элемента, и будет устанавливаться
в качестве текстуры.
Тут есть два способа: мы можем заранее определить в структуре DOM веб-страницы элемент img
и с помощью его атрибута src
установить какое-либо изображение. Либо мы можем динамически в коде javascript создать данный элемент, как продемонстрировано в данном примере.
Поскольку текстура не сразу загружается, то используем обработку события onload
:
image.onload = function() { handleTextureLoaded(image, texture); setupWebGL(); draw(); } image.src = "brickwall.png";
Также мы устанавливаем некоторую картинку. В моем случае это изображение 128х128 brickwall.png. Сама установка текстуры происходит в функции
handleTextureLoaded(image, texture);
. И далее, когда все настройки текстурирования сделаны, происходит отрисовка.
Функция handleTextureLoaded()
выполняет важную роль по настройке всех параметров текстурирования.
function handleTextureLoaded(image, texture) { gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); }
Прежде чем перейти к использованию текстуры, ее надо связать с объектом текстуры texture: gl.bindTexture(gl.TEXTURE_2D, texture);
.
Этот метод действует аналогично вызову gl.bindBuffer()
при связывании буфера вершин.
Используемый далее метод gl.pixelStorei()
указывает далее идущему методу gl.texImage2D()
, как текстура должна
позиционироваться. Так, в данном случае мы передаем в качестве параметра значение gl.UNPACK_FLIP_Y_WEBGL
- этот параметр указывает методу gl.texImage2D(),
что изображение надо перевернуть относительно горизонтальной оси. Для чего это надо?
Все дело в том, что стандартный объект Image, который в данном случае выбран в качестве источника для поставки текстуры, имеет другую систему координат:
Поэтому нам надо совместить две координатные системы, чтобы в будущем было проще работать с текстурой.
Затем можно уже загрузить в текстуру изображение. Это делается с помощью метода gl.texImage2D
.
Полностью параметры этого мы метода мы разберем далее, а пока, я думаю, понятно, что изображение из элемента image загружается в текстуру.
И метод gl.texParameteri
выполняет настройку параметров текстурирования. Первый вызов этого метода устанавливает значение для параметра
gl.TEXTURE_MAG_FILTER
- тем самым мы определяем рендеринг текстуры, если она будет увеличена. Второй вызов метода gl.texParameteri,
наоборот, определяет поведение рендеринг текстуры, если она будет уменьшена.
Чтобы использовать текстуру в шейдере и там ее применить к объекту, нам остается в конце функции setTextures
установить семплер:
shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler"); gl.uniform1i(shaderProgram.samplerUniform, 0);
Семплер будет использоваться для забора из текстуры текселей и совмещения их с пикселями объекта на экране. (Тексель представляет собой пиксель на текстуре.) В вершинном шейдере мы используем следующий код:
attribute vec3 aVertexPosition; varying vec2 vTextureCoords; void main(void) { gl_Position = vec4(aVertexPosition, 1.0); vTextureCoords = vec2(aVertexPosition.x+0.5,aVertexPosition.y+0.5); }
Вектор vTextureCoords затем будет передан во фрагментный шейдер. Здесь же мы устанавливаем координаты:
vTextureCoords = vec2(aVertexPosition.x+0.5,aVertexPosition.y+0.5);
. Поскольку вершины реального объекта расположены в пределах от
(-0.5, -0.5) до (0.5, 0.5) (так мы определили в нашем буфере вершин), то к координатам вершины, передаваемой через атрибут aVertexPosition,
мы прибавляем 0.5. Таким образом. в последствии мы сможем совместить объект с текстурой, у которой все очки находятся в пределах от
(0.0, 0.0) до (1.0, 1.0).
Это, конечно, не самый распространенный способ, и далее мы будем использовать координаты самой текстуры.
А во фрагментном шейдере приводится в действие семплер:
precision highp float; uniform sampler2D uSampler; varying vec2 vTextureCoords; void main(void) { gl_FragColor = texture2D(uSampler, vTextureCoords); }
С одной стороны, кажется, что все вместе, все этапы - так много всего надо делать, но на самом деле не особо то и много, и далее будет еще больше. Итак, суммируя все, можно определить следующие этапы текстурирования:
Определение и создание объекта текстуры с помощью метода gl.createTexture()
.
Создание элемента html, который будет выступать источником для текстуры, например, Image
, и установка его атрибута src.
Определение метода onload
, который будет содержать логику, срабатывающую при загрузке изображения в элемент Image
.
Привязка текстуры с помощью метода gl.bindTexture()
.
Переворачивание текстуры для совмещения ее с координатной системой объекта Image с помощью метода gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
Загрузка текстуры в GPU с помощью метода gl.texImage2D()
.
Установка параметров текстуры с помощью метода gl.texParameteri()
.