Learning WebGL по-русски. Урок 5.

Добро пожаловать на мой пятый урок из серии туториалов WebGL, основанный на шестом уроке NeHe об OpenGL. На этот раз мы собираемся добавить текстуру на трёхмерный объект — это значит, что мы покроем его картинкой, которую мы загрузим из отдельного файла. Это действиткельно очень полезный способ добавить деталей вашей трёхмерной сцене без того, чтобы делать рисуемые объекты слишком сложными. Представьте себе каменную стену в игре-лабиринте. Вы, наверное, не заходите моделировать каждый кирпичик стены как отдельный объект, так что вместо этого вы создаёте картинку с каменной кладкой и покрываете ею стену. Целая стена теперь может быть всего одним объектом.

Вот как будет выглядеть результат:

 

Вот здесь можно поглядеть на "живую" версию.

Наше обычное предупреждение: эти уроки нацелены на людей, обладающих некоторыми навыками программирования, но без действительного опыта в трёхмерной графике. Цель — поднять вас на ноги с хорошим пониманием того, что происходит в коде, чтобы вы могли начать создавать ваши собственные трёхмерные веб-страницы как можно раньше. Если вы ещё не прочитали предыдущие туториалы, вам стоит сначала прочесть их: здесь я только объясню различия в коде между этим и предыдущим уроком.

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

Вы можете посмотреть код этого примера двумя способами. Просто "View Source" в контекстном меню на странице с "живой" версией, или, если вы используете GitHub, вы можете клонировать этот урок (как и все остальные) из этого репозитория. Так или иначе, как только вы добрались до кода, загрузите его в ваш любимый текстовый редактор и взгляните на него.

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

Давайте начнём рассмотрение кода с той части, где происходит загрузка текстуры. Мы вызываем это в самом начале исполнения JavaScript-кода страницы, в низу страницы (новый код обозначен красным):

  function webGLStart() {
    var canvas = document.getElementById("lesson05-canvas");
    initGL(canvas);
    initShaders();
    initBuffers();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);

Давайте рассмотрим initTexture, она находится примерно на трети от начала файла и полностью состоит из нового кода:

  var neheTexture;
  function initTexture() {
    neheTexture = gl.createTexture();
    neheTexture.image = new Image();
    neheTexture.image.onload = function() {
      handleLoadedTexture(neheTexture)
    }

    neheTexture.image.src = "nehe.gif";
  }

Итак, мы создаём глобальную переменную, чтобы хранить в ней текстуру, очевидно, что в реальном примере у вас было бы несколько текстур, и вы бы не использовали глобальные переменные, но у нас пока всё просто. Мы используем gl.createTexture чтобы создать ссылку на текстуру, помещённую в глобальную переменную, затем мы создаём в JavaScript'е объект Image и кладём его в новый атрибут, который мы присоединяем к текстуре, снова используя то преимущества JavaScript'а, благодаря которому мы можем создавать какие угодно поля для любого объекта. Объекты текстур изначально не имеют полей, но для нас удобно одно такое создать. Следующий очевидный шаг — заставить Image загрузить картинку, но сначала мы сделаем функцию обратного вызова (прим. перев. — меня жутко бесит словосочетание "функция обратного вызова", пусть это и устоявшийся термин, так что, с вашего позволения, далее я буду для перевода слова "callback" использовать слово "коллбэк") для загрузки. Она будет вызвана в момент, когда изображение полностью загрузилось, так что безопаснее сделать её в первую очередь. После того, как коллбэк указан, мы указываем Image'у свойство src, и готово. Картинка загрузится асинхронно — то есть код, который указывает src для Image, сработает мгновенно, а фоновый поток загрузит картинку с веб-сервера. Как только это произошло, вызывается коллбэк, в котором вызывается handleLoadedTexture:

  function handleLoadedTexture(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, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.bindTexture(gl.TEXTURE_2D, null);
  }

Первое, что мы делаем — это сообщаем WebGL, что наша текстура — это "текущая" текстура. Текстурные функции WebGL все работают с "текущей" текстурой вместо того, чтобы получать её в аргументах, и bindTexture как раз нужна для указания, какую текстуру считать текущей. Это подобно использованию gl.bindBuffer, которое мы рассматривали ранее.

Далее мы сообщаем WebGL, что все изображения, которые му загружаем в текстуры, должны быть отражены по вертикали. Мы это делаем из-за различий в координатах. Для наших текстурных координат мы используем те, что увеличиваются по мере того, как мы двигаетесь вверх вдоль вертикальной оси, как в обычной математике. Это соответствует координатам X,Y и Z, которые мы используем для указания положений наших вершин. В отличие от этой, большинство других систем компьютерной графики, например, формат GIF, который мы используем для картинки текстуры, используют координаты, которые увеличиваются по мере движения вниз по вертикальной оси. Горизонтальная ось одинакова в обеих координатных системах. Это отличие в вертикальной оси означает, что с точки зрения WebGL, изображение GIF, которое мы используем для нашей текстуры, уже отражено по вертикали, и нам надо "разотразить" его. 

Следующий шаг — загрузить нашу только что загруженное изображение на графическую карту используя texImage 2D. Параметры означают по порядку: какой тип картинки мы используем, уровень детализации (о котором мы поговорим в следующем уроке), формат, в котором текстура будет храниться на видеокарте (повторяется дважды по причинам, которые мы рассмотрим позже), размер каждого "канала" изображения (то есть тип данных, используемый для хранения красного, зелёного и синего цветов) и, наконец, само изображение.

На следующих двух строчках указываются особые параметры масштабирования текстуры. Первая строчка говорит WebGL, что делать, когда текстура заполняет бОльшее пространство экрана относительно её собственного размера. Другими словами, она подсказывает, каким образом её увеличивать. Вторая строка эквивалентна указанию, как её уменьшать. Есть несколько видов масштабирования, которые вы можете указать. NEAREST — самый малопривлекательный из всех, так как он просто говорит, что видеокарта должна использовать оригинальное изображение как оно есть, что означает, что вы увидите пиксельную мозаику, если посмотрите близко. Впрочем, преимущество этого режима в его скорости даже на слабых машинах. В следующем уроке мы посмотрим, как использовать разные режимы масштабирования, чтобы вы могли сравнить их производительность и внешний вид.

После этого мы указываем текущую текстуру на null. Это не строго необходимо, но это хороший стиль. Что-то вроде убирания за собой.

Итак, это был весь код, необходимый для загрузки текстуры. Далее, давайте рассмотрим initBuffers. Из него, конечно, был убран весь код, относящийся к пирамиде, которая у нас была в четвёртом уроке, но более интересное изменение здесь то, что вместо буфера цветов куба у нас теперь другой — буфер текстурных координат. Вот как он выглядит:

    cubeVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    var textureCoords = [
      // Front face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,

      // Back face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Top face
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,

      // Bottom face
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,
      1.0, 0.0,

      // Right face
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
      0.0, 0.0,

      // Left face
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
    cubeVertexTextureCoordBuffer.itemSize = 2;
    cubeVertexTextureCoordBuffer.numItems = 24;

Этот код должен быть вам довольно понятен, как и то, что в нём делается: мы указываем новый повершинный атрибут в буферном массиве, и этот атрибут имеет по два значения на вершину. Эти текстурные координаты указывают, где, в декартовых x и y, вершина находится на текстуре. В соответствии с этими координатами, текстура имеет ширину 1.0 и высоту 1.0., так что (0,0) — это нижний левый угол, в (1,1) — правый верхний. Преобразования между этими размерами и действительным разрешением изображения текстуры выполняется внутри самого WebGL.


Это единственное изменение в initBuffers, так что давайте рассмотрим drawScene. Самые интересные изменения в этой функции, конечно, те, которые заставляют её использовать текстуру. Однако, прежде чем мы этим займёмся, рассмотрим несколько изменений, связанных с довольно простыми вещами, такими как удаление приамиды и того факта, что куб теперь крутится немного по-другому. Я не буду это описывать в деталях, потому что это и так довольно просто. Эти изменения выделены красным в этом куске кода в начале функции drawScene:

  var xRot = 0;
  var yRot = 0;
  var zRot = 0;

  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [0.0, 0.0, -5.0]);

    mat4.rotate(mvMatrix, degToRad(xRot), [1, 0, 0]);
    mat4.rotate(mvMatrix, degToRad(yRot), [0, 1, 0]);
    mat4.rotate(mvMatrix, degToRad(zRot), [0, 0, 1]);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Кроме того есть соответствующие изменения в функции animate для обновления xRot, yRot и zRot, которые я тоже рассматривать не буду.

Итак, когда с этим покончено, давайте взглянем на код текстуры. В initBuffers мы устанавливаем буфер, содержащий текстурные координаты, так что теперь нам надо его привязать к соответствующему атрибуту, чтобы шейдеры могли его видеть:

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, neheTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Тут довольно сложное дело делается. WebGL может работать не более чем 32 текстурами во время любого вызова функции типа gl.drawElements, и они пронумерованы как TEXTURE0 до TEXTURE31. Первые две строчки говорят, что нулевая текстура — это та, которую мы загрузили ранее, а затем третья строка передаёт значение ноль в шейдерную uniform-переменную (которую, как и остальные uniform-переменные, которые мы использовали в матрицах, мы извлекаем из шейдерной программы в initShaders). Это сообщает шейдеру, что мы используем нулевую текстуру. Позже мы рассмотрим, как это работает.

Как только эти три строчки выполнены, всё готово, так что мы используем старый код, чтобы отрисовать треугольники.

Оставшиеся изменения касаются шейдеров. Давайте рассмотрим вначале вершинный шейдер:

  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  varying vec2 vTextureCoord;

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
  }

Это очень похоже на работу с цветом, рассмотренную нами во втором уроке. Всё, что мы тут делаем — это назначаем текстурные координаты (вместо цветов) в качестве повершинного атрибута, и передаём их прямо в varying-переменную.

Как только это будет сделано для каждой вершины, WebGL выработает значения для фрагментов (которые есть не что иное, как пиксели) между вершинами, испольщуя линейную интерполяцию между вершинами — так же, как он делал это с цветами во втором уроке. Итак, фрагмент на полпути между вершинами с текстурными координатами (1,0) и (0,0) выдаст текстурные координаты (0.5, 0), а на полпути между (0,0) и (1,1) выдаст (0.5, 0.5). Следующая остановка — фрагментный шейдер:

  precision mediump float;

  varying vec2 vTextureCoord;

  uniform sampler2D uSampler;

  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
  }

Итак, мы берём интерполированные текстурные координаты, и ещё есть переменная типа sampler, так в шейдере называется текстура. В drawScene наша текстура была привязана на gl.TEXTURE0, а uniform-переменная uSampler была установлена в значение 0, так что этот sampler — и есть наша текстура. Всё, что делает шейдер, это использует функцию texture2D, чтобы получить соответствующий цвет из текстуры, используя координаты. Текстуры традиционно используют s и t для обозначения координат, в отличие от x и y, и шейдерный язык поддерживает такие названия; вместо этого можно было бы использовать vTextureCoord.x и vTextureCoord.y.

Как только у нас есть цвет фрагмента, всё готово! У нас есть затекстуренный объект на экране.

Вот и всё на этот раз. Вот что вы смогли вынести из этого урока: как добавлять текстуры на трёхмерные объекты в WebGL, загружая изображение, сообщая WebGL, что надо его использовать для текстуры, давая вашему объекту текстурные координаты, и используя их и текстуру в шейдерах.

Если у вас есть какие-либо вопросы, комментарии или исправления, пожалуйста оставьте комментарий ниже!

Ну а если нет, зацените следующий урок, в котором я покажу, как сделать базовый клавиатурный ввод в JavaScript, который анимирует вашу трёзмерную сцены, чтобы мы могли заставить её взаимодействовать с пользователем. Мы используем это, чтобы позволить пользователю изменять вращение куба, приближать и удалять его, и редактировать режимы масштабирования текстур WebGL.


You can leave a comment with "Facebook":
Не забывайте оставлять комментарии при помощи "ВКонтакте":
Яндекс.Метрика