пятница, 21 января 2011 г.

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


<< Урок 4

Урок 6 >>




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


Нажмите здесь, если ваш браузер поддерживает WebGL и вы сможете увидеть этот урок "вживую"
Здесь можно почитать как получить браузер с поддержкой WebGL
Более подробно о том, как это работает, ниже


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

В тексте могут встречатья ошибки и неточности. Если вы заметите, что что-то неверно, дайте мне знать об этом в комментариях и мы исправим это как можно скорее.
Есть 2 способа получения исходного кода для этого примера: просмотр кода в браузере, если он поддерживает WebGL и вы просматриваете урок "вживую" в своём браузере, или вы можете скопировать его и другие уроки с GitHub.
В любом случае, после того, как получите код, загрузите его в ваш любимый текстовый редактор и просмотрите.
Ключом к пониманию работы с текстурами является то, что текстуры представляют собой настройки цвета для каждой точки 3D объекта.
Как вы помните из урока 2,цвета указываются при помощи фрагментных шейдеров.
Всё что нам нужно сделать - это загрузить изображение и передать его фрагментому шейдеру.
Фрагментному шейдеру также необходимо знать, какой бит изображения использовать для того фрагмента, с к отрым он работает, поэтому мы также должны предоставить ему эту информацию.
Двайте начнём с просмотра кода, который загружает текстуру. Мы вызываем его прямо в начале выполнения JavaScript кода нашей страницы, в функции webGLStart внизу страницы (новый код выделен красным цветом):
function webGLStart() {
    var canvas = document.getElementById("lesson05-canvas");
    initGL(canvas);
    initShaders();
    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, но для нас это удобно, поэтому мы создали такое свойство.
Очевидно следующим шагом для получение объекта Image является загрузка в него фактического изображения,но перед этим мы присоединим к нему функцию обратного вызова, которая будет вызываться, когда изображение полностью загружено.
Таким образом, нам следует в первую очередь установить эту функцию. Как только это сделано, мы устанавливаем изображению свойство src, теперь всё готово.
Изображение загружается асинхронно, но код, который устанавливает свойство src
изображению немедленно возвращается, а в фоновом потоке происходит загрузка изображения с вебсервера.
Когда изображение загружено, вызывается наша функция обратного вызова, которя вызывает 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, который мы используем для изображения текстуры - используют координаты, которые увеличиваются по мере продвижения вниз по вертикальной оси. Горизонтальная ось одна и та же в обеих системах координат. Различие вертикальных осей означает, что GIF изображение, которое мы использем для нашей текстуры, уже отражено по вертикали и нам необходимо вернуть его обратно в исходное сосстояние.
(Спасибо Ilmari Heikkinen за разъяснение по этому вопроосу в комментариях.)
Следующим шагом является загрузка нашего только что полученного изображения на графическую карту при помощи texImage2D.
Перечислим параметры в порядке их следования:используемое изображение, уровень детализации(что это мы рассмотрим в одном из следующих уроков), формат, в котором мы хотим, чтобы изображение было сохранено на графической карте(повторяю второй раз по причинам, которые вы узнаете позже),размер каждого “канала” изображения(зависит от типа данных, используемого для хранения красной, зелёной и синей цветовой составляющей),
и, наконец, само изображение.
В следующих двух строках указываются параметры масштабирования для текстуры. Первый говорит WebGL каким образом заполнить большое пространство экрана текстурой, если заполняемое пространство больше размера изображения текстуры, другими словами,
в ней указано каким образом отмасштабировать текстуру.
Вторая является инструкцией как обрезать изображение.
Существуют различные способы наложения текстуры, которые вы можете указать, самый простой и непривлекательный из них NEARESTон говорит о том, что оригинальный образ используется "как есть",вследствие чего при просмотре крупным планом она будет выглядеть очень блочно. У этого способа есть преимущество - скорость отображения достаточно хорошая даже на медленных машинах.
В следющем уроке мы рассмотрим использование различных способ наложения текстуры, так что у вас будет возможность сравнить производительность и внешний вид каждого из них.
Как только всё это сделано, мы установим указатель на текущую структуру null.
Это не является строго необходимым, но является хорошей привычкой - очистка после использования.
Итак, это был весь код, который требуется для загрузки текстуры.
Теперь давайте перейдём к initBuffers. Естественно отсутствует весь код, относящийся к пирамиде из урока4, но более интересным изменением является замена буфера цветов вершин куба новым буфером текстурных координат.
Это выглядит следующим образом:
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;
В таком виде код должен показаться вам наглядным, заметьте - всё, что мы сделали - это указали новый аттрибут для каждой вершины в буфере массиве и этот новый аттрибут имеет 2 значения для каждой вершины.
Текстурыне координаты указывают положение x, y для вершины на изображении текстуры.
Размер текстуры нормирован таким образом, что высота и широта изображения принимаются за единицу, так что получаем (0, 0) в левом нижнем углу, (1, 1) в правом верхнем углу.
Это было единственное изменение в 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);
 
    perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
    loadIdentity();
 
    mvTranslate([0.0, 0.0, -5.0])
 
    mvRotate(xRot, [1, 0, 0]);
    mvRotate(yRot, [0, 1, 0]);
    mvRotate(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. То, что мы делаем, сказано в первых 2х строках - нулевая текстура - эта та, которая была загружена ранее, в третьей строке мы передаём нулевое значение в шейдерную форму (которую, как и другие формы,используемые нами для матриц, мы извлекаем из шейдерных программ в 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; 
  }
Это очень похоже на вещи, связанные с цветом, которые мы передавали вершинному шейдеру в уроке2.
Всё, что мы делаем- это определяем текстурные координаты(снова, вместо цвета) в качестве аттрибута для каждой вершины и передаём их в какой-либо переменной.
После того, как это было проделано для каждой вершины, WebGL будет работать с фрагментами(которые, напоминаю, являются просто пикселями), которые находятся между вершинами, вычислив их при помощи линейной интерполяции аналогичному тому, как было проделано с цветами в уроке 2.
Итак, серединный фрагмент между вершинами с текстрными координатами (1, 0) и (0, 0) получит текстурные координаты (0.5, 0), и конечный фрагмент между вершинами (0, 0) and (1, 1) получит текстурные координаты (0.5, 0.5).
Следующая остановка, фрагментный шейдер:
#ifdef GL_ES
  precision highp float;
  #endif
 
  varying vec2 vTextureCoord;
 
  uniform sampler2D uSampler; 
 
  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); 
  }
Таким образом, мы забрали интерполированные значения текстурных координат, и у нас есть переменная типа sampler, которая является способом представления текстур для шейдера.
В функции drawScene, наша текстура это gl.TEXTURE0, а форма uSampler установлена в значение ноль, этот образец представляет нашу текстуру. Всё, что делает шейдер, это использование функции texture2D для получения соответсвующего цвета из текстуры при помощи координат.
Для текстурных координат традиционно использются обозначения s и t чаще. чем x и y, и шейдерный язык поддерживает эти обозначения; мы могли бы так же легко использовать vTextureCoord.x и vTextureCoord.y.
Теперь у нас есть цвет фрагмента! Мы видим на экране текстурированный объект. На сегодня всё. Теперь вы имеете представление о том, как добавлять текстуры к 3D объекту при помощи загрузки изображения, указав WebGL использовать его в качестве текстры, передав вашему объекту текстурные координаты и использование координат и текстур шейдерах.
Если у вас есть вопросы, комментарии или поправки, пожалуйста оставьте комментарий ниже!
Иначе, перейдите к следующему уроку,в котором я покажу, как можно получить простой ввод с клавиатруы в JavaScript, который анимирует вашу 3D сцену и делает её интерактивной для человека, просматривающего страницу.
Мы будем использовать это для предоставления пользователю возможности изменять угол поворота куба, приближать и удалять сцену, и регулировать настройки, переданные WebGL, отвечающие за наложение текстуры.

<< Урок 4

Урок 6 >>



Благодарности: Chris Marrin’s WebKit-only spinning box сильно помогло при написании данного материала, так же как и портирование Крисом демо Jacob Seidelin’s в Firefox.
Как всегда, я глубоко благодарен NeHe за их уроки OpenGL и за скрипты к этому уроку.

7 комментариев:

  1. Заменил картинку с текстурой и ничего не выводится в чём может быть фокус?

    ОтветитьУдалить
  2. вы имеете в виду эту строчку
    neheTexture.image.src = "nehe.gif"; ???

    координаты текстуры проверили?

    ОтветитьУдалить
  3. Координаты текстуры как я понял задаются от 0 до 1 в не зависимости от разрешения файла.
    А с этой строчкой всё понятно картинка подгружается но не отображается.
    Может кто подскажет где можно Доступно почитать о тексурах?

    ОтветитьУдалить
  4. попробуйте здесь http://pmg.org.ru/nehe/nehe06.htm

    ОтветитьУдалить
  5. размер текстуры должен быть кратен двум: например 256 x 256, 512 x 512 итеде

    ОтветитьУдалить
    Ответы
    1. Да. Два в какой-либо степени. Это обычно связывают с оптимизацией.

      Удалить
  6. См. также серию уроков по созданию интерактивных 3D приложений с помощью отечественного WebGL движка Blend4Web.

    http://www.blend4web.com/ru/category/uroki/1/

    ОтветитьУдалить