Знакомство с WebGL

Введение


Статья создана с целью показать основные действия, необходимые для отображения 3d в современном браузере, используя технологию WebGL. Для достижения цели рассмотрим задачу построения нескольких линий в трехмерном пространстве.

Схема работы:
  1. Получаем WebGL контекст из canvas'а.
  2. Загружаем программу шейдеров. А именно:
    • создаем программу шейдоров;
    • получаем исходный код отдельно для вершинного и фрагментного шейдеров;
    • компилируем коды шейдеров;
    • присоединяем к программе;
    • активируем программу.

  3. Устанавливаем две матрицы: model-view и projection.
  4. Размещаем, заполняем, активируем буферы данных вершин.
  5. Рисуем.



1. WebGL-контекст

WebGL контекст возможно получить из DOM-элемента canvas, вызвав метод getContext(“experimental-webgl”). Следует заметить, что Khronos Group рекомендует (https://www.khronos.org/webgl/wiki/FAQ) для получения контекста WebGL использовать следующий способ:

var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
    gl = null;
    for (var ii = 0; ii < names.length; ++ii) {
        try {
            gl = canvas.getContext(names[ii]);
        } catch(e) {}
        if (gl) {
            break;
        }
    }


При успешном получении контекста объект gl имеет методы, названия которых очень похожи на функции OpenGL ES. Например, функция clear(COLOR_BUFFER_BIT) для WebGL будет gl.clear(gl.COLOR_BUFFER_BIT), что очень удобно. Но следует помнить, что не все функции WebGL имеют такой же синтаксис, как и функции OpenGL ES 2.0.

2. Шейдеры

Шейдерная программа является неотъемлемой частью построения изображений с помощью WebGL. Именно через нее задается положение и цвет каждой вершины наших линий. В нашей задаче используется два шейдера: вершинный и фрагментный. При построении линий в трехмерном пространстве вершинный шейдер отвечает за положение вершин в пространстве, основываясь на значениях видовой матрицы и матрицы перспективной проекции. Фрагментный шейдер используется для вычисления цвета наших линий.

Вершинный шейдер

    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;
    uniform mat4 mvMatrix;
    uniform mat4 prMatrix;
    varying vec4 color;

    void main(void) 
    {
	    gl_Position = prMatrix * mvMatrix * vec4 ( aVertexPosition, 1.0 );
	    color       = aVertexColor;
    }


Фрагментный шейдер

     
     #ifdef GL_ES
       precision highp float;
    #endif
    varying vec4 color;
    void main(void) 
    {
        gl_FragColor = color;
    }


То, что определено после «uniform» является общим для всех вершин. Здесь это матрицы преобразования: видовая и перспективная. То, что определено после «attribute» используется при вычислении каждой вершины. Здесь это положение вершины и ее цвет. После «varying» определяем переменную, которая будет передана из вершинного во фрагментный шейдер. Результат вычисления положения присваиваем переменной gl_Position, цвета — gl_FragColor.

3. Модельно-видовая матрица и матрица перспективной проекции

Обе матрицы имеют размер 4х4 и используются для расчета отображения трехмерных объектов на двумерную плоскость. Их различие в том, что видовая матрица определяет, как объекты будут выглядеть для наблюдателя, например, при изменении его положения, а матрица проекции изначально задает способ проецирования.
В нашей программе значения матрицы проекции задаются при вызове функции gluPerspective на этапе инициализации, в дальнейшем эта матрица не меняет своих значений. Функция gluPerspective не является стандартной, ее мы определили сами. Ее аргументы: fovy, aspect, zNear, zFar. fovy — область угла просмотра по вертикали в градусах; aspect — отношение ширины области просмотра к высоте; zNear — расстояние до ближней плоскости отсечения (всё что ближе — не рисуется); zFar — расстояние до дальней плоскости отсечения (всё что дальше — не рисуется).

Для задания значений модельно-видовой матрицы можно использовать несколько подходов. Например, создать и использовать функцию gluLookAt ( camX, camY, camZ, tarX, tarY, tarZ, upX, upY, upZ) – аналог функции для OpenGL, которая принимает в качестве аргументов координаты положение камеры, координаты цели камеры и up-вектор камеры. Другой способ, это создание и использование функций glTranslate, glRotate, glScale, которые производят сдвиг, вращение, масштабирование относительно наблюдателя (камеры). Для первичного определения положения камеры можно использовать gluLookAt, а для последующих преобразований использовать glTranslate, glRotate, glScale. Так или иначе, эти функции лишь изменяют значения одной и той же модельно-видовой матрицы. Для удобства вычисления матриц можно использовать библиотеку sylvester.js, что мы и будем делать.

Теперь, когда нашли способ изменять значения обеих матриц, рассмотрим их передачу программе шейдеров. В нашем вершинном шейдере для модельно-видовой матрицы мы используем переменную «mvMatrix». Чтобы передать этой переменной значения матрицы, нам нужно сначала получить ее индекс в программе. Для этого используем функцию loc=gl.getUniformLocation ( shaderProgram, name ), которая является стандартной. Как несложно догадаться, первый аргумент – переменная, указывающая на программу шейдеров, которая получена на втором этапе, а аргумент «name» — имя переменной, которой мы хотим передать значение, в нашем случае name= «mvMatrix». Теперь, получив индекс, используем функцию gl.uniformMatrix4fv ( loc, false, new Float32Array(mat.flatten())) для передачи значения матрицы mat. Аналогично, получаем индекс и устанавливаем значение для матрицы проекции. Следует помнить, что видовую матрицу в шейдерной программе нужно обновлять всякий раз при изменении ее значений, чтобы они вступили в силу.

4. Буферы данных

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

    /* Создаем буфер  */
    vPosBuffer = gl.createBuffer();
    
    /* Активируем буфер*/
    gl.bindBuffer(gl.ARRAY_BUFFER, vPosBuffer);

    /* Копируем в буфер координаты вершин */
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticies), gl.DYNAMIC_DRAW);
    
    /* Определяем, что координаты вершин имеют определенный индекс атрибута и содержат 3 floats на вершину */
    gl.vertexAttribPointer(vPosLoc, 3, gl.FLOAT, false, 0, 0);

    /* Задействуем индекс аттрибута */
    gl.enableVertexAttribArray(vPosLoc);


Здесь verticies – массив координат точек линий. Координаты идут по 6 штук, первые 3 из которых – x-, y-, z-координата начала линии, следующие, соответственно, конца. vPosLoc – это индекс атрибута «aVertexPosition» в шейдерной программе. Т.к. в нашей программе были явно заданы индексы с помощью gl.bindAttribLocation (shaderProgram, loc, shadersAttrib) на этапе сборки шейдерной программы, то получать их еще раз не нужно. Если бы такого не было, то следует получить индекс, используя команду «vPosLoc = getAttribLocation(shaderProgram, «aVertexPosition»)». Аналогичные действия проводятся и со вторым буфером, отличаться будет данными (вместо verticies массив цветов) и индексом в шейдерной программе (вместо vPosLoc).

5. Рисуем

Очистка буфера цвета или, проще говоря, задание фона произведем стандартными командами

gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);


Теперь выполним рисование
gl.drawArrays(gl.LINES, drawArrayIndexStart, drawArrayLength);

Первый аргумент этой функции говорит, что будем рисовать именно линии, второй – индекс в буфере, с которого начнем рисование, drawArrayLength – количество элементов для рисования. Т.к. мы в буфер передаем координаты вершин из массива verticies, то

drawArrayLength = verticies.length / 3;


Если у нас изменились прямые, то выполняем шаги 4, 5 для перерисовки. Если у нас изменилось положение камеры, то выполняем шаг 3 и шаг 5.

Заключение


Задача построения прямых линий взята не с потолка. Есть программа, которая решает систему дифференциальных уравнений и строит результат в 3d, используя OpenGL. Было решено перенести программу на php и отображать результат, используя WebGL. Для решения задачи об отображении в трехмерном пространстве прямых были изучены современные engine из списка (http://ru.wikipedia.org/wiki/WebGL): WebGLU, GLGE, C3DL, Copperlicht, SpiderGL и SceneJS. Для этого был создан интерфейс, позволяющий универсализировать общение основной программы со сторонними движками. Результатов удалось добиться с WebGLU, C3DL. В других либо отсутствует простой способ построения линии, либо он неоптимальный. В одном из них функция линии задокументирована, но на форуме проекта дали понять, что использовать ее не удастся, и предложили, рисовать ее многоугольниками.

К сожалению, при использовании C3DL еще не удалось оптимизировать процесс, что привело к низкому значению fps. При работе с WebGLU была допущена ошибка, которая так же повлияла на значение fps. Это заставило написать свой движок, который сейчас и используется. Я ни в коем случае не хочу упрекнуть сторонние движки, они созданы для более широкого круга задач, чем простое рисование линий.

Пару слов о браузерах. Тестировалось на Firefox 4 beta 8, Chrome 8 с –enable-webgl. На данной задаче Firefox показывал значение fps выше Chrome в 1,5-2 раза. При обновлении Chrome до beta 9 показатели не изменились. Показатели fps не изменились и при обновлении Firefox beta 8 до beta 9, разве что в консоли стало больше непонятных ошибок и стало некорректно отображаться сцена с использованием WebGLU.

Ссылки на рабочую версию




Список литературы


Поделиться публикацией

Комментарии 15

    +6
    Вот оно преимущество OpenGL — занимаясь разработкой на C++ с использованием OpenGL в качестве хобби совершенно без затруднений все понял. :)
      +1
      Спасибо за статью, отличное введение. Отдельно за простые и понятные примеры :)
        0
        Помоему, будущее WebGL, если подходить со стороны JavaScript, сомнительное.
        1. Разработчики игр не используют JavaScript как основной язык и поэтому им проще портировать игру(если они собираются выпустить не нескольких платформах), написанную, например на C++, под NaCl. Если цель только браузеры, то им проще доучить своих C++ разработчиков и опять же написать на NaCl.
        2. NaCl на много быстрее чем JavaScript и имеет поддержку множества платформ от ARM до x64.
        Минус NaCl для проприетарных игр в том, что его исходники компилируются на клиенте «can only use code compiled to the host's native instruction set». Не особо слежу за NaCl, но возможно сделают свой байткод.

        Спасибо, если надумаете писать продолжение остановитесь подробнее на шейдерах это, прям, темный лес.
          +2
          Дело не в JavaScript, а в кроссплатформенности.
          Сейчас большинство онлайн-игр (с 3Д) работают через плагины и чаще только под Windows.
          А так, нужно только открыть сайт и запустить игру (в идеале).
            +2
            Тем не менее на чистом JavaScript мы из какой-нибудь современной ММО с кучей частиц, шейдерами, тенями и отражениями получим лагодром, да и не каждый разработчик будет открывать код. Если что-нибудь не тяжелое(объёмные карты, казуальные игры), то WebGL отличная замена Flash. Я клоню к тому, что JavaScript+WebGL не для современных игр.
              0
              Flash чаще canvas заменяет.
              Разработчики браузеров в каждым выпуском ускоряют работу JS. Уже сейчас можно эмулировать SNES на чистом JS.
              К примеру некоторые разработчики переносят на WebGL игру MineCraft (ради фана). Свою нишу WebGL точно займет.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Шейдеры и тени к javascript имеют довольно опосредованное отношение — если в коде на С оно не тормозило, то и в js-варианте не будет, обсчитывает видеокарта же.

                Вот физику приличную, как в современных халфлайфах, на js скорее всего придется очень серьезно оптимизировать.
                  +2
                  А физику тоже на шейдеры вынесут и не будут париться :)
                  В принципе до устаканивания openCL все и развлекались на фрагмент програм
            0
            А что вы думаете о перспективе развития направления WebGL? Его применении и будущем?
              +3
              Думаю для web'а нужен более высокоуровневый API для работы с 3d, когда видишь простой вызов OpenGLных функций из javascript то прям плакать хочется).
                +1
                Можно тот же SDL портировать, например. Только загрузчик текстур придется серьезно довести напильником.
                0
                Я убежден, что у WebGL есть шансы на достойное положение в будущем. И в этом мне помогает убедиться его настоящие. Уже сейчас WebGL предлагает нам картины, смотря на которые возникает чувство радости и немного гордости.
                Вот примеры (одни из моих любимых):

                Эти и другие примеры, а так же ссылки по теме можно найти на Planet WebGL

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое