WebGL Урок 2 — Добавляем цвет

Урок 3 >> << Урок 1

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

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

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

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

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

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

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

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

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

  • Определили вершинный и фрагментный шейдеры, используя тэги HTML — <script> c типами «x-shader/x-vertex» и «x-shader/x-fragment»
  • Инициализировали контекст WebGL в функции initGL
  • Загрузили шейдеры в объект программы WebGL через функции getShader и initShaders
  • Определили матрицу модель-вид mvMatrix и проекционную матрицу pMatrix и передали их из JavaScript в WebGL, чтобы шейдеры смогли их видеть
  • Заполнили буферы информацией об объектах сцены в функции initBuffers
  • Нарисовали саму сцену в соответствующей функции drawScene
  • Определили функцию webGLStart, где последовательно запустили функции инициализации
  • И, наконец, написали минимум HTML, чтобы было где отобразить все это

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

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

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

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

После того, как вершинный шейдер отработал, WebGL делает магическое преобразование 3D-изображение из varying-переменных в 2D-изображение и затем вызывает фрагментный шейдер для каждого пикселя изображения (поэтому в некоторых графических 3D-системах вы можете услышать понятие «пиксельный шейдер»). Несомненно, фрагментный шейдер вызывается для пикселей, которые не совпадают с вершинами — для тех, которые расположены между теми пикселями, которые лежат на вершинах. Таким образом, он заполняет точки в позициях между вершинами методом линейной интерполяции. В нашем случае заполняется пространство, ограниченное тремя точками, в результате чего получается видимый треугольник. Назначение фрагментного шейдера — вернуть цвет для каждой интерполируемой точки, что он и делает через установку значения varying-переменной gl_FragColor.

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

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

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

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

А теперь давайте взглянем на код. Мы разберем отличия от первого урока. Во-первых, вершинный шейдер. Он претерпел довольно много изменений и вот его новый код:


  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-переменная для каждого вершинного шейдера) тем же способом, что и в первом уроке, а все, что мы делаем с цветом — передаем его из входного атрибута прямо в выходную 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");
  }

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

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

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

В следующий раз мы добавим код для анимации сцены, который будет вращать наши треугольник и квадрат.

Урок 3 >> << Урок 1

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

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