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

Продолжаем переводить замечательный туториал "Learning WebGL". Сегодня у нас перевод второго урока:

Добро пожаловать на мой второй туториал о WebGL! На этот раз мы собираемся рассмотреть, как сделать сцену цветной. Этот урок основан на туториале №3 OpenGL от NeHe.
 



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




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

Ниже — о том, как же это всё работает...

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

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

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

Большая его часть в целом та же, что и в первом уроке. Пройдясь от начала до конца, мы:
 


Единственные вещи, которые изменились в этом коде с первого урока — это шейдеры и функции initBuffers и drawScene. Чтобы объяснить, как эти изменения работают, вам необходимо узнать немного о процессе отрисовки в WebGL. Вот диаграмма:



Диаграмма показывает, в весьма упрощённой форме, как данные, переданные в функции JavaScript'а в drawScene превращаются в пиксели в канвасе WebGL на экране. Она показывает только шаги, необходимые, чтобы объяснить этот урок. Мы рассмотрим более подробную версию этой диаграммы в будущих уроках.

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

Он делает это, вызывая вершинный шейдер единожды для каждой вершины, каждый раз с атрибутами, установленными соответственно вершине. Uniform-переменные также передаются внутрь, но, как предполагает их название, они не меняются от вызова к вызову. Вершинный шейдер выполняет действия с этими данными — в уроке 1 они применял матрицы проекции и модели-вида, чтобы вершины все могли быть в перспективе и в положении, соответствующем текущему состоянию модели-вида — и кладёт результаты в штуковины, называнные varying-переменные. Он может выдать несколько varying-переменных. В частности, одна из них обязательна — gl_Position, которая содержит в себе координаты вершины после того, как шейдер закончил с ней делать дела.

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

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

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

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

Удобно, что это даёт нам "бесплатно"  градиент цветов. Все varying-переменные, установленные вершинным шейдером, линейно интерполируются, когда генерируются фрагменты между вершинами, а не только позиции. Линейная интерполяция цвета между вершинами даёт нам сглаженные градиенты навроде тех, что вы видите на треугольнике на картинке выше.

Давайте рассмотрим код. Мы пройдёмся по изменениям касательно урока 1. Сначала вершинный шейдер. Он довольно сильно изменился, так что вот новый код:

attribute vec3 aVertexPosition;
  attribute vec4 aVertexColor;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;

  varying vec4 vColor;

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


Тут говорится, что мы имеем два атрибута — входные значения, которые меняются от вершины к вершине — названные aVertexPosition и aVertexColor, две неизменные uniform-переменные, названные uMVMatrix и uPMatrix, и одно выходное значение в форме varying-переменной, названной vColor.

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

Как только всё это будет отработано для каждой вершины, интерполяция готова сделать нам фрагменты, они и передаются во фрагментный шейдер:


  precision mediump float;

  varying vec4 vColor;

  void main(void) {
    gl_FragColor = vColor;
  }



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

Вот и все различия в шейдерах между этим уроком и предыдущим. Но есть ещё два изменения. Первое очень маленькое: в 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);

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

    shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
    gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);


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

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

Оставшиеся изменения в этому уроке находятся в initBuffers, который теперь нуждается в установке буферов как для позиций вершин, так и для их цветов, и в drawScene, которой теперь нужно передать их оба в WebGL.

Давайте сначала рассмотрим initBuffers: мы описываем глобальные переменные чтобы держать в них буфера для треугольника и квадрата:

  var triangleVertexPositionBuffer;
  var triangleVertexColorBuffer;
  var squareVertexPositionBuffer;
  var squareVertexColorBuffer;

Затем, сразу как мы создали буфер позиций вершин треугольника, мы указываем цвета его вершин:

  function initBuffers() {
    triangleVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    var vertices = [
         0.0,  1.0,  0.0,
        -1.0, -1.0,  0.0,
         1.0, -1.0,  0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    triangleVertexPositionBuffer.itemSize = 3;
    triangleVertexPositionBuffer.numItems = 3;

    triangleVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    var colors = [
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    triangleVertexColorBuffer.itemSize = 4;
    triangleVertexColorBuffer.numItems = 3;


Итак, значения, которые мы предоставили для цветов находятся в списке, один набор значений для каждой вершины, точно так же как и в случае с их позициями. Однако есть ещё одно интересное различие медлу двумя буферными массивами: в то время как позиции вершин указаны в виде трёх значений для каждой, для координат X, Y и Z, их цвета указаны каждый в виде четырёх элементов — красный, зелёный, синий и альфа. Альфа, если вы с ней не знакомы, есть мера непрозрачности (0 — прозрачный, 1 — полностью непрозрачный) и будет полезна в следующих уроках. Это изменение в числе элементов на одну вершину отражено также в изменении itemSize, ассоциированного с ним.

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

squareVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    vertices = [
         1.0,  1.0,  0.0,
        -1.0,  1.0,  0.0,
         1.0, -1.0,  0.0,
        -1.0, -1.0,  0.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    squareVertexPositionBuffer.itemSize = 3;
    squareVertexPositionBuffer.numItems = 4;

    squareVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    colors = []
    for (var i=0; i < 4; i++) {
      colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    squareVertexColorBuffer.itemSize = 4;
    squareVertexColorBuffer.numItems = 4;


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

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, [-1.5, 0.0, -7.0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);


    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);


    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
  }

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

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

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