Использование WebGL в Dart

Знакомство с WebGL в языке Dart

30 April 2014 г. 19:43:44

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

Для работы с WebGL в языке Dart есть встроенная библиотека WebGL, которая позволяет выводить интерактивную графику на холст. По сути это врапер поверх WebGL для javascript.

Исходный код примера можно найти в гитхабе. Все шаги можно увидеть в истории комитов.

Связывание WebGL с контекстом холста

Создание шейдеров

Передача данных в шейдеры

Вывод объекта

Преобразование объектов и камеры

Поворот объектов

Анимация

Связывание WebGL с контекстом холста

Для начала необходимо создать canvas в html-е и связать его с контекстом WebGL. Первым делом добавим библиотеку dart:web_gl, создадим для нее псевдоним WebGL, чтобы не путаться. WebGL не имеет высокоуровневых функций для работы с матрицами и векторами, поэтому чтобы облегчить себе жизнь добавим еще несколько библиотек, dart:typed_data и package:vector_math/vector_math.dart.

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

 gl = canvas.getContext('webgl');
//в FireFox контекст называется experimental-webgl
if (gl == null)
gl = canvas.getContext("experimental-webgl");

Контекст будет сохраняться в свойстве объекта . Далее посылаем окну сообщение что можно отрисовывать холст, и передаем ему функцию отрисовки.

window.requestAnimationFrame(render);

Функция отрисовки будет вызвана в будущем, но нам надо ее создать. Эта функция не будет делать ничего особенного просто зальет холст определенным цветом. Контекст gl берется из свойства объекта WebGLScene.

 void render(double time){
gl.viewport(0, 0, canvas.width, canvas.height);
//зальем канвас черным цветом
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(WebGL.COLOR_BUFFER_BIT);
}

И теперь мы можем любоваться прекрасным черным прямоугольником на экране.

Создание шейдеров

Для вывода любых объектов в WebGL придется создавать шейдер.

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

WebGL использует 2 вида шейдеров

  • Шейдер вершин vertexShader, для обработки вершин геометрических форм и

  • Фрагмент шейдер framentShader для определения цветов для каждого пикселя. при помощи этого шейдера настраивается свет, цвет и другие характеристики графики.

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

Можно заметить что фрагмент шейдер зависит от шейдера вершин. И необходим механизм для передачи данных из одного шейдера в другой. Для этого в WebGL предусмотрено 3 типа переменных

  • attributes, которые доступны только в шейдере вершин. И содержит атрибуты вершин выводимого объекта, масштаб смещение, цвет вершины, нормали и т.д.

  • uniforms, доступны как в шейдере вершин, так и в фрагмент шейдере

  • varyings, доступны только во фрагментом шейдере.

Схема работы шейдеров показана ниже:

Схема работы шейдеров в WebGL

Язык GLSL достаточно сложен, однако нам не понадобится многое из него. Для начала попробуем вывести простой прямоугольник произвольного цвета, для этого нам просто понадобиться передать координаты объекта, сам объект в виде массива вершин (vertexBuffer) и массив индексов(indexBuffer) и цвет объекта.

Координаты передаются через атрибут aPosition в vertexShader, а цвет передается во fragmentShader через униформ uColor.

таким образом шейдеры будут выглядеть следующим образом:

vertexShaderCode = """
precision highp float;
attribute vec3 aPosition;
void main() {
gl_Position = vec4(aPosition, 1);
}""";
fragmentShaderCode = """
precision highp float;
uniform vec4 uColor;
void main() {
gl_FragColor =uColor;
}""";

далее попробуем скомпилировать шейдеры из строк, и в случае возникновение ошибок выдавать сообщение об ошибке.

void compile(){
    vertexShader = gl.createShader(WebGL.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexShaderCode);
    gl.compileShader(vertexShader);
    if (!gl.getShaderParameter(vertexShader, WebGL.COMPILE_STATUS)){
      throw gl.getShaderInfoLog(vertexShader);
}
fragmentShader = gl.createShader(WebGL.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderCode);
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(fragmentShader, WebGL.COMPILE_STATUS)){
throw gl.getShaderInfoLog(fragmentShader);
}
program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
if (!gl.getProgramParameter(program, WebGL.LINK_STATUS)){
throw gl.getProgramInfoLog(program);
    }
}

в результате скомпилированный шейдер будет храниться в переменной program. Эту программу можно будет использовать при создании геометрии.

поэтому создадим экземпляр шейдера в WebGLPuzzle в конструкторе

quadShader = new Shader(this.gl);

Вывод объекта

Для вывода прямоугольника создадим класс Quad. в начале он будет состоять только из конструктора и функции отрисовки. В конструкторе инициализируем все необходимые для работы данные. Контекст и шейдер объект будет получать параметрах конструктора.

Далее необходимо создать вершинный буфер и буфер индексов. Буфферы в WebGL создаются следующим образом:

  • во первых надо создать переменную типа буффер vertexBuffer = gl.createBuffer();

  • во-вторых связать контекст ввода с буффером gl.bindBuffer(WebGL.ARRAY_BUFFER, vertexBuffer); (указать тип буффера и буффер данных),

  • и наконец заполнить буффер данными

  • также хорошим тоном считается отвязывани буффера, для этого используется комманда вида gl.bindBuffer(WebGL.ARRAY_BUFFER, null);

    Quad(this.gl, this.shader){ gl.useProgram(shader.program);

    vertexBuffer = gl.createBuffer();
    gl.bindBuffer(WebGL.ARRAY_BUFFER, vertexBuffer);
    gl.bufferDataTyped(WebGL.ARRAY_BUFFER, new Float32List.fromList([
    -0.0, -0.0, 0.0,
    1.0, -0.0, 0.0,
    1.0, 1.0, 0.0,
    -0.0, 1.0, 0.0
    ]), WebGL.STATIC_DRAW);
    indexBuffer = gl.createBuffer();
    gl.bindBuffer(WebGL.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(WebGL.ELEMENT_ARRAY_BUFFER, new Int16List.fromList([0,1,2,0,2,3]), WebGL.STATIC_DRAW);
    

Передача данных в шейдеры

Инициализируем атрибут aPosition при помощи метода vertexAttribPointer.

void vertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLintptr offset)

Функция vertexAttribPointer определяет свойства массива данных атрибута вершин и содержит следующие параметры:

  • indx — индекс атрибута.

  • size — количество элементов атрибута: 1, 2, 3 или 4.

  • type:

  • BYTE

  • UNSIGNED_BYTE

  • SHORT

  • UNSIGNED_SHORT

  • FIXED

  • FLOAT

  • normalized — если true, тогда целые значения приводятся к диапазону [-1,1] или [0,1] при конвертации в значения с плавающей запятой.

  • stride — байтовый интервал между атрибутами.

  • offset — номер первого элемента массива.

    
    
    class Quad{
      WebGL.RenderingContext gl;
      Shader shader;
      int aPosition;
      WebGL.Buffer vertexBuffer, indexBuffer;
      
      Quad(this.gl, this.shader){
        gl.useProgram(shader.program);
    
        aPosition       = gl.getAttribLocation(shader.program, 'aPosition');
        vertexBuffer = gl.createBuffer();
        gl.bindBuffer(WebGL.ARRAY_BUFFER, vertexBuffer);
        gl.bufferDataTyped(WebGL.ARRAY_BUFFER, new Float32List.fromList([
                                                                         -0.0, -0.0, 0.0,
                                                                          1.0, -0.0, 0.0,
                                                                          1.0, 1.0, 0.0,
                                                                         -0.0, 1.0, 0.0
                                                                         ]), WebGL.STATIC_DRAW);
        gl.vertexAttribPointer(aPosition, 3, WebGL.RenderingContext.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(aPosition);
        
        indexBuffer = gl.createBuffer();
        gl.bindBuffer(WebGL.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(WebGL.ELEMENT_ARRAY_BUFFER, new Int16List.fromList([0,1,2,0,2,3]), WebGL.STATIC_DRAW);
      }
      
      render(){
        //Передаем в униформ красный цвет
        gl.uniform4f(gl.getUniformLocation(shader.program, 'uColor'), 1.0, 0.0, 0.0, 1.0);
        gl.drawElements(WebGL.TRIANGLES, 6, WebGL.UNSIGNED_SHORT, 0);
      }  
    }
    

В функции render передаем цвет прямоугольника при помощи униформа.

На экране появится картинка следующего вида:

Это не очень похоже на квадрат размером 1x1. Это получилось потому, что по умолчанию холст считается размером от -1 до +1 что можно изобразить следующим образом.

Но как добиться того чтобы геометрия была нужных пропорций? для этого Придется познакомится с преобразованием объекта и преобразованием камеры

Преобразование объектов и камеры

Следующим шагом попробуем создать прямоугольник определенного размера и в определенном месте. Это можно сделать разными способами, самый простой способ, это манипулирование координатами вершин. Но пересчитывать координаты - не самое веселое занятие, поэтому воспользуемся другим способом. В шейдере вершин мы можем изменять местоположение объектов, менять масштаб и т.д. используя матричные преобразования. Плохая новость в том, что в WebGL нет понятия камеры мы не можем сместить ее, все что мы можем это использовать матричные преобразования и передавать матрицы в шейдер. Вторая плохая новость состоит в том, что в WebGL нет методов для работы с векторами и матрицами, все преобразования надо выполнять вручную. Однако в арсенале Dart есть библиотеки dart:typed_data и vector_math, которые имеют множество методов для работы с матрицами, что значительно упрощает задачу. Для решения этой задачи нам понадобятся 2 типа матриц:

  • Матрица камеры. (или по другому model-view матрица). Значение этой матрицы применяется ко всей сцене в целом

  • Матрица объекта. Значение этой матрицы применяется к каждому объекту сцены. Шейдер вершин будет выглядеть следующим образом: vertexShaderCode = """ precision highp float;

    attribute vec3 aPosition;
    uniform mat4 uCameraMatrix;
    uniform mat4 uObjectMatrix;
    void main() {
      gl_Position = uCameraMatrix*uObjectMatrix*vec4(aPosition, 1);
    }""";
    

Добавлены 2 переменные униформ типа матрица 4*4. Перемножая матрицы с aPosition можно изменять размеры и местоположение прямоугольника. Можно запустить приложение и убедиться что все работает, как и прежде.

Таким образом можно изменить конструктор Quad и передать координаты прямоугольника и его размеры и цвет

Quad(this.gl, this.shader, this.x, this.y, this.w, this.h, this.color)

создадим экземпляр красного квадрата следующим образом:

quad_red = new Quad(gl, quadShader, 100, 100, 20, 40, new Vector4(1.0, 0.0, 0.0, 1.0));

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

Часть этих задач будет решать матрица камеры, потому что в частности необходимо чтобы масштаб всех объектов был одинаков.

Для камеры необходимо сделать следующее:

  • Изменить масштаб таким образом чтобы масштаб совпадал с высотой и шириной холста

  • Сместить начало вывода с точки в центе, в левый верхний угол.

  • Направить положительную часть оси Y вниз (по умолчанию он направлен вверх)

Матрица объекта должна переместить прямоугольник в нужное место и изменить его размеры.

Каждому этому действию соответствует определенное матричное преобразование:

создадим матрицы идентичности, и убедимся что ничего не изменилось

cameraMatrix.setIdentity();

Переместим вывод на -1 по X и Y, чтобы верхний левый угол соответствовал точке (0, 0). для этого применим трансформацию translate

cameraMatrix.translate(-1.0, -1.0);

теперь сделаем так, чтобы ось Y была направлена вниз, это облегчит вывод, все координаты будут считаться от левого верхнего угла. Для этого умножим матрицу камеры, на матрицу отражения. Матрица отражения это матрица в которой в соответствующем элементе на диагонали установлено -1 если надо отразить, и 1 если остается без изменений.

cameraMatrix.multiply( new Matrix4.identity().setDiagonal(new Vector4(1.0, -1.0, 1.0, 1.0)));

Далее изменим масштаб таким образом, чтобы пространство от нуля до одного соответствовало одному пикселю. Для этого в Dart есть матричное преобразование scale. масштабируем пропорционально ширине и высоте холста.

cameraMatrix.scale(2.0/ canvas.width, 2.0 / canvas.height);

На этом преобразования матрицы камеры, закончены. можно передавать матрицу в шейдер.

gl.uniformMatrix4fv(gl.getUniformLocation(quadShader.program, "uCameraMatrix"), false, cameraMatrix.storage);

Создание матрицы преобразований объекта аналогично матрице камеры. Создадим матрицу идентичности и перенесем прямоугольник в точку с координатами (x, y)

objMatrix.setIdentity();
objMatrix.translate(1.0*x+PUZZLE_SIZE/2, 600.0 - PUZZLE_SIZE - y - PUZZLE_SIZE/2);

Все что осталось, это изменить масштаб таким образом чтобы прямоугольник был нужного размера. Масштабируем при помощи метода scale пропорционально высоте и ширине.

objectMat.scale(1.0w, 1.0h); передаем матрицу в шейдер и насладимся получившимся результатом.

gl.uniformMatrix4fv(gl.getUniformLocation(shader.program, "uObjectMatrix"), false, objectMat.storage);

Поворот объектов

В библиотеке Dart для поворота предусмотрены соответствующие функции rotate, добавим в Quad свойство angle и создадим несколько прямоугольников.

Можно заметить что прямоугольники поворачиваются относительно их левого верхнего угла, а хотелось бы чтобы они вращались относительно центра (или произвольной точки. В OpenGL, для этого можно использовать функции PopMatrix и PushMatrix, однако в WebGL таких функций нет, все что мы можем делать это матричные преобразование. Но и этого вполне достаточно, просто для поворота понадобится сделать чуть больше действий. алгоритм вращения вокруг определенной точки следующий

  1. Создать матрицу перемещения (rotate_tmp) и переместить матрицу объекта в нужную точку, в нашем случае в центр прямоугольника

    Matrix4 rotate_tmp = new Matrix4.identity(); rotate_tmp.translate(w/2, h/2);

  2. Умножить матрицу перемещения на матрицу объекта

    objectMat.multiply(rotate_tmp);

  3. Выполнить поворот, при этом поворот будет выполнен относительно центра

    objectMat.rotateZ(angle * Math.PI / 180.0);

  4. Найти обратную матрицу перемещения rotate_tmp.invert();

  5. Умножить ее на матрицу объекта, тем самым вернувшись после поворота в исходную точку

    objectMat.multiply(rotate_tmp);

Поворот будет выполнен относительно центра.

Анимация

Сделаем анимацию вращения для каждого из прямоугольников. Для этого будем вызывать функцию window.requestAnimationFrame каждые 30 мили-секунд. Решить эту задачу поможет библиотека dart:async в которой есть прекрасные функции для работы с асинхронными вызовами. В частности понадобится объект Timer, который будет выполнять необходимые операции с заданным интервалом.

new Async.Timer(new Duration(milliseconds: 30), () => window.requestAnimationFrame(render));

Добавив этот код в функцию render мы будем запрашивать перерисовку кадра каждые 30 милисекунд. При каждом вызове функции render объекта увеличиваем угол.

quads.forEach((q){
q.angle = q.angle + 0.3;
q.render();
});

Посмотреть итоговый пример можно здесь.


Оставьте свой комментарий

comments powered by Disqus
Меню

Cult of digits 2014 Яндекс.Метрика