WebGL Урок 5 — Введение в текстуры

Урок 6 >> << Урок 4

Материал в оригинале можно найти здесь

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

Вот как выглядит результат урока в браузере с поддержкой WebGL:

Здесь можно посмотреть онлайн-демонстрацию, если ваш браузер поддерживает WebGL. Здесь можно узнать, что делать, если браузер не поддерживает WebGL.

Теперь о том, как это все работает…

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

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

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

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

Что ж, начнем с просмотра кода, который загружает текстуры. Мы вызываем его сразу при начале выполнения JavaScript на нашей странице, в функции webGLStart в низу страницы (новый код выделен красным):


  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 для создания ссылки на текстуру, которую запоминаем в глобальной переменной, а затем создаем объект Image в JavaScript и помещаем его в новый атрибут объекта текстуры, снова пользуясь преимуществом JavaScript устанавливать любое поле любому объекту. Объект текстуры не содержит поле для изображения по умолчания, но нам удобно его иметь и поэтому мы его создаем. Следующий шаг, очевидно, — загрузить само изображение, которое будет содержаться в объекте Image, но перед этим мы назначаем ему функцию обратного вызова. Она вызовется, когда картинка целиком загрузится, поэтому надежней всего сначала назначить колбэк. После этого мы устанавливаем свойство 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-изображение, которое мы используем, уже перевернуто по вертикали и нам нужно «расперевернуть» его. (Спасибо Ilmari Heikkinen за прояснение этого в комментариях.)

Следующим шагом будет помещение загруженного изображения в пространство видеокарты с помощью texImage2D. Параметры по порядку их передачи: тип используемого изображения, уровень детализации (что мы рассмотрим в следующих уроках), формат хранения в видеокарте (указан дважды по причине, которую мы также рассмотрим позже), размер каждого «канала» изображения (тип данных для хранения красного, зеленого и синего) и наконец само изображение.

Следующие две строки устанавливают специальные параметры масштабирования текстуры. Первый говорит 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. Наиболее интересные изменения касаются, конечно же, тех, которые добавляют использование текстуры. Однако, перед тем, как мы окунемся в это, есть несколько изменений, относящихся к действительно простым вещам — таким как удаление пирамиды и вращение куба в разные стороны. Не буду описывать все подробно, так как все это можно легко понять. Изменения выделены красным в этом отрывке кода:


  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. Здесь в первых двух строках мы говорим, что текстура под номером 0 — та, которую мы загрузили ранее, затем в третьей строке мы передаем значение ноль в 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, и эта uSampler представляет нашу текстуру. Все, что делает шейдер, — использует функцию texture2D для получения соответствующего цвета из текстуры, используя координаты. Текстуры традиционно используют s и t для их координат вместо x и y, но язык шейдера поддерживает их псевдонимами. Поэтому мы могли бы так же легко использовать vTextureCoord.x и vTextureCoord.y.

Теперь, когда у нас есть цвет для фрагмента, мы все сделали! У нас есть текстурированный объект на экране.

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

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

В противном случае, переходите к следующему уроку, в котором я покажу, как вы можете добавить элементарное управление клавишами в функцию JavaScript, которая анимирует 3D-сцену, чтобы начать взаимодействие с человеком, который просматривает веб-страницу. Это позволит пользователю изменять вращение куба, приближаться и отдаляться и изменять способы наложения текстур.

Урок 6 >> << Урок 4

9 thoughts on “WebGL Урок 5 — Введение в текстуры

  • 15.02.2016 at 12:00
    Permalink

    Спасибо за замечательный перевод!

    Скажите пожалуйста как можно совместить 4-ый и 5-ый уроки в одной сцене

    Есть две фигуры с цветом и одна с текстурой. а шейдер один на всех, и если я пытался как то обусловить в шейдере закраску пикселя либо цветом либо текстурой, то он ругался на то, либо на другое, ставя в «-1» индекс того атрибута, который, как ему показалось, не будет использован.

    Ответить
    • 15.02.2016 at 21:38
      Permalink

      Лучше использовать отдельные шейдеры для цвета и текстуры. Но если все-таки нужно обойтись одним шейдером, то можно пойти на хитрость и в шейдере всегда умножать цвет на текстуру:
      gl_FragColor = texture2D(uSampler, vTextureCoord.st) * uColor.

      При отрисовке объектов с текстурами используем код:

      var whiteColor = new Float32Array([1, 1, 1, 1]);

      gl.uniform4fv(uColorLocation, whiteColor);
      gl.bindTexture(gl.TEXTURE_2D, someTexture);

      В этом случае цвет текстуры умножается на единицу и остается цветом текстуры. Для отрисовки объектов с цветом создаем текстуру из одного белого пикселя:

      var whiteTexture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, whiteTexture);
      var whitePixel = new Uint8Array([255, 255, 255, 255]);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, whitePixel);

      gl.uniform4fv(uColorLocation, customColor);
      gl.bindTexture(gl.TEXTURE_2D, whiteTexture);

      Соответственно, теперь цвет умножается на белую текстуру и остается цветом объекта.

      Ответить
  • 29.09.2016 at 21:39
    Permalink

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE,image);

    вот здесь возникает ошибка, Chrome ругается:
    —————————————————————————
    SecurityError: Failed to execute ‘texImage2D’ on ‘WebGLRenderingContext’: The cross-origin image at file:///C:/jo/sky.jpg may not be loaded.
    —————————————————————————

    Ответить
    • 30.09.2016 at 23:12
      Permalink

      Такова политика безопасности Chromium, когда вы открываете файл с диска. Хотя в Firefox работает. Для того, чтобы работало и в хроме, установите веб-сервер (Apache, IIS или что вам по душе) и запустите через http://localhost/lessons/lesson05/ — пример заработает.

      Ответить
  • 23.01.2017 at 14:31
    Permalink

    Если требуется наложить на каждую сторону квадрата свою картинку в качестве текстуры, как переключаться между изображениями при указании координат?

    Ответить
    • 23.01.2017 at 21:30
      Permalink

      Обычно вам не нужно переключаться между разными картинками. Делается одна картинка, состоящая из нескольких, а затем всё управляется через текстурные координаты.
      Посмотрите на работу с текстурами здесь http://webglfundamentals.org/webgl/lessons/webgl-3d-textures.html — в конце статьи как раз рассматривается куб с разными картинками на разных гранях.
      К сожалению, статья пока на английском языке, но я активно работаю над переводом этого блога и скоро доберусь и до текстур.

      Ответить
  • 27.07.2017 at 09:44
    Permalink

    Пожалуйста, помогите понять почему у меня возникает такая ошибка:
    [.Offscreen-For-WebGL-0000000005642CC0]RENDER WARNING: texture bound to texture unit 0 is not renderable. It maybe non-power-of-2 and have incompatible texture filtering.

    Ответить
    • 31.07.2017 at 22:40
      Permalink

      Это предупреждение есть и в уроке, видимо автор недоглядел. Предупреждение возникает по той причине, что рендеринг начинает выполняться, когда изображение для текстуры ещё не загружено с сервера. Изображение грузится асинхронно и занимает определённое время, а отрисовка сцены начинается сразу при загрузке страницы (на событие body.onload). Вот в эти несколько моментов и генерируются предупреждения. Чтобы обойти эту проблему, достаточно перенести вызов функции tick из webGLStart в конец функции handleLoadedTexture. В этом случае рендеринг начнётся только после загрузки изображения и предупреждение исчезнет.

      Ответить

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *