Spec-Zone.ru › Web APIs

Пример базовой 2D-анимации WebGL

В этом примере WebGL мы создаём холст и на нём отрисовываем вращающийся квадрат с помощью WebGL. Система координат, используемая для представления сцены, совпадает с системой координат холста. То есть, (0, 0) находится в верхнем левом углу, а нижний правый угол находится в координатах (600, 460).

Пример вращающегося квадрата

Давайте рассмотрим шаги по созданию вращающегося квадрата.

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

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

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

  uniform vec2 uScalingFactor;
  uniform vec2 uRotationVector;

  void main() {
    vec2 rotatedPosition = vec2(
      aVertexPosition.x * uRotationVector.y +
            aVertexPosition.y * uRotationVector.x,
      aVertexPosition.y * uRotationVector.y -
            aVertexPosition.x * uRotationVector.x
    );

    gl_Position = vec4(rotatedPosition * uScalingFactor, 0.0, 1.0);
  }
</script>

Основная программа предоставляет нам атрибут aVertexPosition, который представляет позицию вершины в используемой системе координат. Нам нужно преобразовать эти значения таким образом, чтобы обе компоненты позиции находились в диапазоне от -1.0 до 1.0. Это можно легко сделать, умножив на коэффициент масштабирования, основанный на коэффициенте формы контекста. Мы увидим это вычисление вскоре.

Мы также вращаем фигуру, и это можно сделать здесь, применив преобразование. Сначала мы это сделаем. Повернутая позиция вершины вычисляется путём применения вектора вращения, который находится в униформе uRotationVector, вычисленной кодом JavaScript.

Затем окончательная позиция вычисляется путём умножения повернутой позиции на вектор масштабирования, предоставленный кодом JavaScript в uScalingFactor. Значения z и w фиксированы на 0.0 и 1.0 соответственно, поскольку мы рисуем в 2D.

Стандартная переменная WebGL gl_Position затем устанавливается в значение преобразованной и повернутой позиции вершины.

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

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

<script id="fragment-shader" type="x-shader/x-fragment">
  #ifdef GL_ES
    precision highp float;
  #endif

  uniform vec4 uGlobalColor;

  void main() {
    gl_FragColor = uGlobalColor;
  }
</script>

Это начинается с указания точности типа float, как требуется. Затем мы устанавливаем глобальную переменную gl_FragColor в значение униформы uGlobalColor, которая устанавливается кодом JavaScript в цвет, используемый для отрисовки квадрата.

HTML

HTML состоит только из <canvas>, на котором мы получим контекст WebGL.

<canvas id="gl-canvas" width="600" height="460">
  Oh no! Your browser doesn't support canvas!
</canvas>

Глобальные переменные и инициализация

Сначала глобальные переменные. Мы не будем их обсуждать здесь; вместо этого мы поговорим о них по мере использования в коде.

let gl = null;
let glCanvas = null;

// Aspect ratio and coordinate system
// details

let aspectRatio;
let currentRotation = [0, 1];
let currentScale = [1.0, 1.0];

// Vertex information

let vertexArray;
let vertexBuffer;
let vertexNumComponents;
let vertexCount;

// Rendering data shared with the
// scalers.

let uScalingFactor;
let uGlobalColor;
let uRotationVector;
let aVertexPosition;

// Animation timing

let shaderProgram;
let currentAngle;
let previousTime = 0.0;
let degreesPerSecond = 90.0;

Инициализация программы обрабатывается обработчиком события load, который называется startup():

window.addEventListener("load", startup, false);

function startup() {
  glCanvas = document.getElementById("gl-canvas");
  gl = glCanvas.getContext("webgl");

  const shaderSet = [
    {
      type: gl.VERTEX_SHADER,
      id: "vertex-shader",
    },
    {
      type: gl.FRAGMENT_SHADER,
      id: "fragment-shader",
    },
  ];

  shaderProgram = buildShaderProgram(shaderSet);

  aspectRatio = glCanvas.width / glCanvas.height;
  currentRotation = [0, 1];
  currentScale = [1.0, aspectRatio];

  vertexArray = new Float32Array([
    -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5,
  ]);

  vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);

  vertexNumComponents = 2;
  vertexCount = vertexArray.length / vertexNumComponents;

  currentAngle = 0.0;

  animateScene();
}

После получения контекста WebGL, gl, нам нужно начать с построения программы шейдеров. Здесь мы используем код, позволяющий легко добавлять несколько шейдеров в нашу программу. Массив shaderSet содержит список объектов, каждый из которых описывает одну функцию шейдера, которая должна быть скомпилирована в программу. Каждая функция имеет тип (один из gl.VERTEX_SHADER или gl.FRAGMENT_SHADER) и идентификатор (идентификатор элемента <script>, содержащего код шейдера).

Набор шейдеров передаётся в функцию buildShaderProgram(), которая возвращает скомпилированную и связанную программу шейдеров. Мы рассмотрим, как это работает далее.

После построения программы шейдеров мы вычисляем коэффициент формы нашего контекста, разделив его ширину на высоту. Затем мы устанавливаем текущий вектор вращения для анимации в [0, 1], а вектор масштабирования в [1.0, aspectRatio]. Вектор масштабирования, как мы видели в вершинном шейдере, используется для масштабирования координат для соответствия диапазону от -1.0 до 1.0.

Далее создаётся массив вершин в виде Float32Array с шестью координатами (три 2D-вершины) на треугольник, для отрисовки в общей сложности 12 значений.

Как вы можете видеть, мы используем систему координат от -1.0 до 1.0 для каждой оси. Почему, спросите вы, нам нужно вносить какие-либо изменения? Это потому, что наш контекст не квадратный. Мы используем контекст шириной 600 пикселей и высотой 460. Каждая из этих размерностей отображается в диапазоне от -1.0 до 1.0. Поскольку длины двух осей не равны, если мы не скорректируем значения одной из двух осей, квадрат будет растянут в одном направлении или другом. Поэтому нам нужно нормализовать эти значения.

После создания массива вершин мы создаём новый буфер GL для их хранения, вызывая gl.createBuffer(). Мы связываем стандартную ссылку на буфер массива WebGL с ним, вызывая gl.bindBuffer(), а затем копируем данные вершин в буфер с помощью gl.bufferData(). Указано использование подсказки gl.STATIC_DRAW, сообщая WebGL, что данные будут установлены только один раз и никогда не изменятся, но будут использоваться многократно. Это позволяет WebGL рассмотреть любые оптимизации, которые он может применить, чтобы повысить производительность на основе этой информации.

Теперь, когда данные вершин предоставлены WebGL, мы устанавливаем vertexNumComponents в количество компонентов в каждой вершине (2, так как это 2D-вершины) и vertexCount в количество вершин в списке вершин.

Затем текущий угол поворота (в градусах) устанавливается в 0.0, так как мы ещё не выполнили поворот, а скорость вращения (в градусах за период обновления экрана, обычно 60 кадров в секунду) устанавливается в 6.

Наконец, вызывается animateScene() для отрисовки первого кадра и планирования отрисовки следующего кадра анимации.

Компиляция и связывание программы шейдеров

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

function buildShaderProgram(shaderInfo) {
  const program = gl.createProgram();

  shaderInfo.forEach((desc) => {
    const shader = compileShader(desc.id, desc.type);

    if (shader) {
      gl.attachShader(program, shader);
    }
  });

  gl.linkProgram(program);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.log("Error linking shader program:");
    console.log(gl.getProgramInfoLog(program));
  }

  return program;
}

Сначала вызывается gl.createProgram() для создания новой пустой программы GLSL.

Затем для каждого шейдера в указанном списке шейдеров мы вызываем функцию compileShader() для компиляции, передавая ей идентификатор и тип функции шейдера для построения. Каждый из этих объектов, как упоминалось ранее, содержит идентификатор элемента <script>, в котором находится код шейдера, и тип шейдера.

Скомпилированный шейдер прикрепляется к программе шейдеров путём передачи его в gl.attachShader().

Примечание: Мы могли бы сделать здесь ещё один шаг и посмотреть на значение атрибута <script> элемента type для определения типа шейдера.

После компиляции всех шейдеров программа связывается с помощью gl.linkProgram().

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

Наконец, скомпилированная программа возвращается вызывающему методу.

Компиляция отдельного шейдера

Функция compileShader() ниже вызывается функцией buildShaderProgram() для компиляции отдельного шейдера.

function compileShader(id, type) {
  const code = document.getElementById(id).firstChild.nodeValue;
  const shader = gl.createShader(type);

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

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.log(
      `Error compiling ${
        type === gl.VERTEX_SHADER ? "vertex" : "fragment"
      } shader:`,
    );
    console.log(gl.getShaderInfoLog(shader));
  }
  return shader;
}

Код извлекается из HTML-документа путём получения значения текстового узла, содержащегося в элементе <script> с указанным идентификатором. Затем создаётся новый шейдер указанного типа с помощью gl.createShader().

Исходный код отправляется в новый шейдер путём передачи его в gl.shaderSource(), а затем шейдер компилируется с помощью gl.compileShader()

Ошибки компиляции регистрируются в консоли. Обратите внимание на использование шаблона строки для вставки правильной строки типа шейдера в генерируемое сообщение. Фактические данные об ошибке получаются путём вызова gl.getShaderInfoLog().

Наконец, скомпилированный шейдер возвращается вызывающему методу (которым является функция buildShaderProgram()).

Отрисовка и анимация сцены

Функция animateScene() вызывается для отрисовки каждого кадра анимации.

function animateScene() {
  gl.viewport(0, 0, glCanvas.width, glCanvas.height);
  gl.clearColor(0.8, 0.9, 1.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  const radians = (currentAngle * Math.PI) / 180.0;
  currentRotation[0] = Math.sin(radians);
  currentRotation[1] = Math.cos(radians);

  gl.useProgram(shaderProgram);

  uScalingFactor = gl.getUniformLocation(shaderProgram, "uScalingFactor");
  uGlobalColor = gl.getUniformLocation(shaderProgram, "uGlobalColor");
  uRotationVector = gl.getUniformLocation(shaderProgram, "uRotationVector");

  gl.uniform2fv(uScalingFactor, currentScale);
  gl.uniform2fv(uRotationVector, currentRotation);
  gl.uniform4fv(uGlobalColor, [0.1, 0.7, 0.2, 1.0]);

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

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

  gl.enableVertexAttribArray(aVertexPosition);
  gl.vertexAttribPointer(
    aVertexPosition,
    vertexNumComponents,
    gl.FLOAT,
    false,
    0,
    0,
  );

  gl.drawArrays(gl.TRIANGLES, 0, vertexCount);

  requestAnimationFrame((currentTime) => {
    const deltaAngle =
      ((currentTime - previousTime) / 1000.0) * degreesPerSecond;

    currentAngle = (currentAngle + deltaAngle) % 360;

    previousTime = currentTime;
    animateScene();
  });
}

В первую очередь, для отрисовки кадра анимации необходимо очистить фон до нужного цвета. В данном случае мы устанавливаем область просмотра на основе размера <canvas>, вызываем clearColor() для установки цвета, используемого при очистке содержимого, а затем очищаем буфер с помощью clear().

Далее вычисляется текущий вектор вращения путем преобразования текущего значения угла поворота в градусах (currentAngle) в радианы, а затем установка первой компоненты вектора вращения на синус этого значения, а второй компоненты — на косинус. Вектор currentRotation теперь представляет собой координаты точки на единичной окружности, расположенной под углом currentAngle.

useProgram() вызывается для активации программы шейдинга GLSL, которую мы определили ранее. Затем мы получаем расположения каждого из униформ, используемых для обмена информацией между кодом JavaScript и шейдерами (с помощью getUniformLocation()).

Униформа с именем uScalingFactor устанавливается в значение currentScale, вычисленное ранее; как вы, возможно, помните, это значение используется для корректировки системы координат в зависимости от соотношения сторон контекста. Это выполняется с помощью uniform2fv() (поскольку это 2-компонентный вектор с плавающей точкой).

uRotationVector устанавливается в текущий вектор вращения (currentRotation)) также с помощью uniform2fv().

uGlobalColor устанавливается с помощью uniform4fv() в цвет, который мы хотим использовать для отрисовки квадрата. Это 4-компонентный вектор с плавающей точкой (по одной компоненте для красного, зеленого, синего и альфа-каналов).

Теперь, когда это всё сделано, мы можем настроить буфер вершин и нарисовать нашу фигуру. Сначала буфер вершин, которые будут использованы для отрисовки треугольников фигуры, устанавливается вызовом bindBuffer(). Затем индекс атрибута позиции вершины получается из программы шейдера вызовом getAttribLocation().

После того, как индекс атрибута позиции вершины стал доступен в aVertexPosition, мы вызываем enableVertexAttribArray() для включения атрибута позиции, чтобы он мог использоваться программой шейдера (в частности, вершинным шейдером).

Затем буфер вершин связывается с атрибутом aVertexPosition вызовом vertexAttribPointer(). Этот шаг не очевиден, поскольку эта связь почти побочный эффект. Но в результате доступ к aVertexPosition теперь получает данные из буфера вершин.

Установив связь между буфером вершин нашей фигуры и атрибутом aVertexPosition, используемым для передачи вершин по одной в вершинный шейдер, мы готовы нарисовать фигуру, вызвав drawArrays().

На этом этапе кадр отрисован. Всё, что осталось сделать, это запланировать отрисовку следующего. Это делается вызовом requestAnimationFrame(), который запрашивает выполнение обратного вызова в следующий раз, когда браузер готов обновить экран.

Наш requestAnimationFrame() обратный вызов получает в качестве входного параметра единственный параметр, currentTime, который указывает время начала отрисовки кадра. Мы используем его, а также сохранённое время, в котором был отрисован предыдущий кадр, previousTime, вместе с количеством градусов в секунду, на которое должен поворачиваться квадрат (degreesPerSecond), для вычисления нового значения currentAngle. Затем значение previousTime обновляется, и мы вызываем animateScene() для отрисовки следующего кадра (и, в свою очередь, планируем отрисовку следующего кадра, и так до бесконечности).

Результат

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

См. также

  • WebGL API
  • Учебник по WebGL

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Basic_2D_animation_example

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API