Learning WebGL по-русски. Урок 1 часть 2.

Уж больше года назад я взялся переводить туториал Learning WebGL.
Перевёл первую часть урока, да на том дело и остановилось.

Учёба, знаете ли, работа, да и вообще жизнь не особо располагала.
И вот, бросив работу, я решил всё же вернуться к этой теме.
Тем более, что на русский язык этот цикл туторов так никто до конца и не перевёл.

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

Итак, собственно:


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

Всё ещё со мной? Спасибо :-) Давайте сначала разберёмся с самыми скучными функциями. Первая из них — initGL и вызывается из webGLStart. Она где-то в самом начале страницы, вот, скопирую сюда, чтоб было куда поглядеть:

var gl;
  function initGL(canvas) {
    try {
      gl = canvas.getContext("experimental-webgl");
      gl.viewportWidth = canvas.width;
      gl.viewportHeight = canvas.height;
    } catch(e) {
    }
    if (!gl) {
      alert("Could not initialise WebGL, sorry :-(");
    }
  }


Это очень просто. Как вы, возможно, уже заметили, функции initBuffers и drawScene часто обращаются к объекту, названному gl, который, понятное дело, связан с какой-то основнополагающей WebGL-"штукой". Эта функция берёт ту "штуку", которая названа WebGL-контекстом, и делает это, запрашивая канвас, заданный контексту, используя стандартное имя контекста. (Как вы уже, возможно  догадались, в какой-то момент имя контекста сменится с “experimental-webgl” to “webgl”. Я обновлю этот урок и постану в блог, когда это произойдёт. Подписывайтесь на RSS-ленту, если хотите узнать об этом — и, конечно же, если вы хотите как-минимум-раз-в-недельные новости WebGL) Как только мы получили контекст, мы снова используем готовность JavaScript'а позволить нам установить любое свойство, какое мы пожелаем любому объекту в начале функции drawScene. Как только это сделано, наш GL-контекст готов.

После вызова initGL, webGLStart вызвал initShaders. Это, ясен пень, инициализирует шейдеры (хм ;-) Мы к этому вернёмся позже, потому что сначала нам нужно рассмотреть нашу матрицу модели-вида и матрицу проекции, которую я упомянул ранее. Вот код:

var mvMatrix = mat4.create();
  var pMatrix = mat4.create();

Итак, мы описываем переменную, названную mvMatrix чтобы содержать в себе матрицу модели-вида и ещё одну, названную pMatrix, для матрицы проекции, и затем для начала назначаем в них нулевые матрицы. Тут стоит немного подробнее рассказать о матрице проекции. Как вы помните, мы применили glMatrix-функцию mat4.perspective к этой переменной, чтобы установить нашу перспективу, прямо в самом начале drawScene. Это было потому что WebGL не поддерживает перспективу напрямую, так же как и не поддерживает матрицу модели-вида. Но так де как и процесс перемещения вещей и поворота их, которые содержатся в матрице модели-вида, процесс делания вещей, которые далеко, пропорционально мельче, чем те, что рядом, так же является той вещью, которые хорошо представить в виде матриц. И, как вы уже без сомнения, догадались сейчас, матрица проекции — это именно то, что это делает. Функция mat4.perspective, с её соотношением сторон и полем зрения, породила нам матрицу со значениями, которые дали нам ту перспективу, которую мы и хотели.

Итак, сейчас мы рассмотрели всё кроме функции setMatrixUniforms, которая, как я сказал ранее, двигает матрицы проекции и модели-вида из JavaScript'а в WebGL, и тех страшных вещей, связанных с шейдерами. Они взаимосвязаны, так что давайте начнём чуть издалека.

Теперь в спросите: что же такое шейдер? Ну, на каком-то этапе истории трёхмерной графики они могли быть именно тем, о чём говорит их название: кусками кода, которые говорили системе, как затенять или окрашивать части сцены до её отрисовки. Однако, со временем сфера их деятельности значительно выросла, так что сейчас их лучше определить как куски кода, которые могут сделать абсолютно что угодно с частями сцены до её отрисовки. И это действительно очень полезно, потому что (а) они выполняются на видеокарте, так что они делают своё дело быстро и (б) те трансформации, на которые они способны, могут быть действительно удобны даже в таком простом примере, как этот.

Причина, по которой мы вводим шейдеры в то, что, казалось бы, должно быть простым примером WebGL (а они как минимум являются темой "не для нубов" в OpenGL-туториалах) является то, что мы хотим заставить WebGL-систему, надеюсь, работающую на графических картах, применить нашу матрицу модели-вида и нашу матрицу проекции к нашей сцене без того, чтобы нам двигать каждую точку и каждую вершину в (относительно) медленном JavaScript'е. Это ужасно полезно и стоит того, чтобы забежать немного вперёд.

Итак, вот как они у нас устроены. Как вы помните, webGLStart вызывает initShaders, так что давайте пройдёмся по ним шаг за шагом:

var shaderProgram;
  function initShaders() {
    var fragmentShader = getShader(gl, "shader-fs");
    var vertexShader = getShader(gl, "shader-vs");

    shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      alert("Could not initialise shaders");
    }

    gl.useProgram(shaderProgram);

Как видите, она использует функцию getShader, чтобы получить две вещи, "фрагментный шейдер" и "вершинный шейдер", а затем суёт их обе в WebGL-штуку, названную "программа". Программа — это кусок кода, который "живёт" "внутри" WebGL. Можете рассматривать его как способ указания чего-либо, что может выполняться на графической карте. Как вы могли ожидать, вы можете ассоциировать с ней некоторое количество шейдеров, каждый из которых представлен в виде отрывка кода внутри программы. А именно, каждая программа может содержать в себе один фрагментный и один вершинный шейдер. Рассмотрим их.

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);


Как только функция запилила нам программу и воткнула в неё шейдеры, она обращается к "атрибуту", который она хранит в новом поле объекта программы, названный vertexPositionAttribute. И снова мы применяем преимущество JavaScript'а, которое нам даёт возможность сделать любое поле в любом объекте. Объекты программы не имеют изначально поля vertexPositionAttribute, но нам будет удобно держать оба значения вместе, так что мы просто сделаем атрибут новым полем объекта программы.

    shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
  }

Итак, для чего же нужен vertexPositionAttribute? Как вы, возможно, помните, мы его использовали в drawScene. Если вы снова посмотрите на код, который устанавливает позиции вершин треугольника из соответствующего буфера, вы увидите, что тамошний код связал буфер с тем атрибутом. Скоро поймёте зачем. А пока давайте просто запомним, что мы также используем gl.enableVertexAttribArray чтобы сообщить WebGL, что мы хотим предоставить значения для атрибута, используя массив.

Последнее, что делает initShaders, так это получает ещё два значения из программы, это положения двух вещей, названных uniform-переменными. Скоро мы с ними встретимся. Пока что просто запомните, что как и атрибут, мы для удобства храним их в объекте программы.

Теперь давайте рассмотрим getShader:

  function getShader(gl, id) {
      var shaderScript = document.getElementById(id);
      if (!shaderScript) {
          return null;
      }

      var str = "";
      var k = shaderScript.firstChild;
      while (k) {
          if (k.nodeType == 3)
              str += k.textContent;
          k = k.nextSibling;
      }

      var shader;
      if (shaderScript.type == "x-shader/x-fragment") {
          shader = gl.createShader(gl.FRAGMENT_SHADER);
      } else if (shaderScript.type == "x-shader/x-vertex") {
          shader = gl.createShader(gl.VERTEX_SHADER);
      } else {
          return null;
      }

      gl.shaderSource(shader, str);
      gl.compileShader(shader);

      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          alert(gl.getShaderInfoLog(shader));
          return null;
      }

      return shader;
  }

Это ещё одна функция из тех, что выглядят куда сложнее, чем есть на самом деле. Всё, что мы делаем здесь — это ищем элемент на нашей HTML-странице, который имеет ID, соответствующий переданному аргументу, суём в него содержимое, создаём фрагментный или вершинный шейдер в зависимости от их типа (больше про различие между ними — в следующих уроках) и потом передаём это в WebGL для компиляции в форму, которая может выполняться на видеокарте. Затем код обрабатывает все ошибки, и готово! Конечно, мы могли просто объявить шейдеры как строки в нашем JavaScript-коде и не возиться с извлечением их из HTML, но так, как у нас сделано, лучше читается потому что они выглядят как скрипты на странице, как будто бы они сами являются JavaScript'ом.

Теперь давайте взглянем на код шейдера:

<script id="shader-fs" type="x-shader/x-fragment">
  precision mediump float;

  void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
</script>

<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 aVertexPosition;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
  }
</script>

Первое, что надо запомнить о них, так это то, что они написаны не на JavaScript'е, несмотря на то, что язык на него очень похож. На самом деле, они написаны на специальном языке — названном GLSL — который очень много позаимствовал у Си (как, конечно, и сам JavaScript).

Первый из двух, фрагментный шейдер, в целом ничего не делает. В нём есть немного обязательного шаблонного кода, который говорит видеокарте, насколько точные нам нужны числа с плавающей запятой (средняя точность хороша, потому что она поддерживается везде, где поддерживается WebGL, а значение highp не работает на мобилках), а потом просто указывает, что всё, что отрисовывается, будет отрисовано белым цветом. (Как обращаться с цветами здесь будет рассказано в следующих уроках).

Второй шейдер чуть поинтереснее. Это вершинный шейдер — что, как вы помните, означает, что это есть кусок кода для видеокарты, который может делать с вершинами практически всё что угодно. Связанный с этим, он имеет две uniform-переменных, названных uMVMatrix and uPMatrix. Uniform-переменные полезны потому что к ним можно обратиться извне шейдера — конечно, в том числе и из содержащей его программы, как вы, возможно, помните из того момента, когда мы вытаскивали их расположение в initShaders, и из кода, который мы рассмотрим далее, где (я уверен, вы уже поняли) мы указали им в качестве значений матрицы модели-вида и проекции. Вам может захотеться думать о шейдерной программе как об объекте (в объектно-ориентированном смысле) и о uniform-переменных как о полях.

Теперь шейдер вызывается для каждой вершины, и вешрины передаются в шейдерный код как aVertexPosition, благодаря использованию vertexPositionAttribute в drawScene, когда мы ассоциировали атрибут с буфером. Небольшой кусок кода в функции main шейдера просто перемножает позицию вершины на матрицы модели-вида и проекции и выдаёт в результате финальную позицию вершины.

Итак, webGLStart вызвал initShaders, который использовал getShader чтобы загрузить фрагментный и вершинный шейдеры из скриптов на веб-странице, чтобы они могли быть откомпилированы и переданы в WebGL, а позже использованы для рендера нашей трёхмерной сцены.

И после всего этого, остаётся только один нерассмотренный код — setMatrixUniforms, который легко понять, когда вы знаете всё вышенаписанное :-)

function setMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
  }


Итак, используя обращения к uniform-переменным, которые представляют наши матрицы проекции и модели-вида, которые мы получили в initShaders, мы отсылаем WebGL значения из наших JavaScript'овых матриц.

Фух! Это довольно много для первого урока, но, надеюсь, вы (и я) поняли все основы, которые необходимы нам для начала строительства чего-то более интересного — цветных, движущихся, правильных трёхмерных WebGL-моделей. Чтобы узнать больше, читайте далее второй урок.


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