Spec-Zone.ru › Web APIs

Добавление 2D-контента в контекст WebGL

  • Предыдущая
  • Следующая

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

Полный исходный код этого проекта доступен на GitHub.

Включение библиотеки glMatrix

В этом проекте используется библиотека glMatrix для выполнения матричных операций, поэтому вам необходимо включить её в свой проект. Мы загружаем копию с CDN.

Примечание: Обновите свой файл "index.html" следующим образом:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>WebGL Demo</title>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"
      integrity="sha512-zhHQR0/H5SEBL3Wn6yYSaTTZej12z0hVZKOv3TwCUXT1z5qeqGcXJLLrbERYRScEDDpYIJhPC1fk31gqR783iQ=="
      crossorigin="anonymous"
      defer></script>
    <script src="webgl-demo.js" type="module"></script>
  </head>

  <body>
    <canvas id="gl-canvas" width="640" height="480"></canvas>
  </body>
</html>

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

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

Шейдеры

Шейдер — это программа, написанная на языке OpenGL ES Shading Language (GLSL), которая получает информацию о вершинах, составляющих фигуру, и генерирует данные, необходимые для отрисовки пикселей на экране: в частности, позиции пикселей и их цвета.

При отрисовке контента WebGL выполняются две функции шейдера: вершинный шейдер и фрагментный шейдер. Вы пишете их на GLSL и передаёте текст кода в WebGL для компиляции и выполнения на графическом процессоре. Вместе набор вершинного и фрагментного шейдеров называется программой шейдеров.

Давайте кратко рассмотрим два типа шейдеров, с учётом примера отрисовки 2D-фигуры в контексте WebGL.

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

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

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

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

Наш вершинный шейдер ниже получает значения координат вершин из атрибута, который мы определяем как aVertexPosition. Затем эта позиция умножается на две матрицы 4x4, которые мы предоставляем, называемые uProjectionMatrix и uModelViewMatrix; gl_Position устанавливается в результат. Для получения дополнительной информации о проекции и других матрицах вы можете найти эту статью полезной: .

Примечание: Добавьте этот код в вашу функцию main().

// Vertex shader program
const vsSource = `
    attribute vec4 aVertexPosition;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    void main() {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    }
  `;

Стоит отметить, что мы используем атрибут vec4 для позиции вершины, который фактически не использует 4-компонентный вектор; то есть, он может обрабатываться как vec2 или vec3 в зависимости от ситуации. Но когда мы выполняем вычисления, нам нужно, чтобы он был vec4, поэтому вместо преобразования его в vec4 каждый раз, когда мы выполняем вычисления, мы будем использовать vec4 с самого начала. Это устраняет операции из каждого вычисления, которое мы выполняем в нашем шейдере. Производительность важна.

В этом примере мы вообще не вычисляем освещение, так как ещё не применили его к сцене. Это будет сделано позже, в примере Освещение в WebGL. Обратите также внимание на отсутствие работы с текстурами здесь; это будет добавлено в Использование текстур в WebGL.

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

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

В данном случае мы возвращаем белый цвет каждый раз, так как мы просто рисуем белый квадрат без применения освещения.

Примечание: Добавьте этот код в вашу функцию main().

const fsSource = `
    void main() {
      gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
  `;

Инициализация шейдеров

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

Примечание: Добавьте эти две функции в скрипт "webgl-demo.js".

//
// Initialize a shader program, so WebGL knows how to draw our data
//
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

  // Create the shader program

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  // If creating the shader program failed, alert

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert(
      `Unable to initialize the shader program: ${gl.getProgramInfoLog(
        shaderProgram,
      )}`,
    );
    return null;
  }

  return shaderProgram;
}

//
// creates a shader of the given type, uploads the source and
// compiles it.
//
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);

  // Send the source to the shader object

  gl.shaderSource(shader, source);

  // Compile the shader program

  gl.compileShader(shader);

  // See if it compiled successfully

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert(
      `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`,
    );
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

Функция loadShader() принимает в качестве входных данных контекст WebGL, тип шейдера и исходный код, а затем создаёт и компилирует шейдер следующим образом:

  1. Новый шейдер создаётся с помощью вызова gl.createShader().
  2. Исходный код шейдера отправляется в шейдер с помощью вызова gl.shaderSource().
  3. После того, как шейдер получил исходный код, он компилируется с помощью gl.compileShader().
  4. Чтобы убедиться в успешной компиляции шейдера, проверяется параметр шейдера gl.COMPILE_STATUS. Для получения его значения вызывается gl.getShaderParameter(), указывая шейдер и имя параметра, который мы хотим проверить (gl.COMPILE_STATUS). Если это false, мы знаем, что компиляция шейдера завершилась неудачно, поэтому отображается всплывающее сообщение с информацией от компилятора, полученной с помощью gl.getShaderInfoLog(), затем шейдер удаляется и возвращается null для указания сбоя загрузки шейдера.
  5. Если шейдер был загружен и успешно скомпилирован, скомпилированный шейдер возвращается вызывающей стороне.

Примечание: Добавьте этот код в вашу функцию main().

// Initialize a shader program; this is where all the lighting
// for the vertices and so forth is established.
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

После создания программы шейдеров нам нужно найти местоположения, которые WebGL назначил нашим входным данным. В данном случае у нас есть один атрибут и две униформы. Атрибуты получают значения из буферов. Каждый проход вершинного шейдера получает следующее значение из буфера, назначенного этому атрибуту. Униформы аналогичны глобальным переменным JavaScript. Они сохраняют одно и то же значение для всех проходов шейдера. Поскольку местоположения атрибутов и униформ специфичны для одной программы шейдеров, мы сохраним их вместе, чтобы их было легко передавать.

Примечание: Добавьте этот код в вашу функцию main().

// Collect all the info needed to use the shader program.
// Look up which attribute our shader program is using
// for aVertexPosition and look up uniform locations.
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
  },
};

Создание квадратной плоскости

Прежде чем мы сможем отрисовать нашу квадратную плоскость, нам нужно создать буфер, который содержит её вершинные позиции, и поместить в него вершинные позиции.

Мы сделаем это с помощью функции, которую мы назовём initBuffers(), которую мы реализуем в отдельном модуле JavaScript. По мере изучения более сложных концепций WebGL этот модуль будет дополнен для создания большего — и более сложного — 3D-объектов.

Примечание: Создайте новый файл с именем "init-buffers.js", и поместите в него следующий код:

function initBuffers(gl) {
  const positionBuffer = initPositionBuffer(gl);

  return {
    position: positionBuffer,
  };
}

function initPositionBuffer(gl) {
  // Create a buffer for the square's positions.
  const positionBuffer = gl.createBuffer();

  // Select the positionBuffer as the one to apply buffer
  // operations to from here out.
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

  // Now create an array of positions for the square.
  const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];

  // Now pass the list of positions into WebGL to build the
  // shape. We do this by creating a Float32Array from the
  // JavaScript array, then use it to fill the current buffer.
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

  return positionBuffer;
}

export { initBuffers };

Эта процедура довольно проста, учитывая основную природу сцены в этом примере. Она начинается с вызова метода createBuffer() объекта gl для получения буфера, в который мы будем хранить вершинные позиции. Затем он связывается с контекстом путём вызова метода bindBuffer().

После этого мы создаём массив JavaScript, содержащий позиции каждой вершины квадратной плоскости. Затем он преобразуется в массив чисел с плавающей точкой и передаётся в метод bufferData() объекта gl для задания вершинных позиций объекта.

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

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

Примечание: Создайте новый файл под названием "draw-scene.js" и вставьте в него следующий код:

function drawScene(gl, programInfo, buffers) {
  gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
  gl.clearDepth(1.0); // Clear everything
  gl.enable(gl.DEPTH_TEST); // Enable depth testing
  gl.depthFunc(gl.LEQUAL); // Near things obscure far things

  // Clear the canvas before we start drawing on it.

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // Create a perspective matrix, a special matrix that is
  // used to simulate the distortion of perspective in a camera.
  // Our field of view is 45 degrees, with a width/height
  // ratio that matches the display size of the canvas
  // and we only want to see objects between 0.1 units
  // and 100 units away from the camera.

  const fieldOfView = (45 * Math.PI) / 180; // in radians
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 0.1;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();

  // note: glMatrix always has the first argument
  // as the destination to receive the result.
  mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);

  // Set the drawing position to the "identity" point, which is
  // the center of the scene.
  const modelViewMatrix = mat4.create();

  // Now move the drawing position a bit to where we want to
  // start drawing the square.
  mat4.translate(
    modelViewMatrix, // destination matrix
    modelViewMatrix, // matrix to translate
    [-0.0, 0.0, -6.0],
  ); // amount to translate

  // Tell WebGL how to pull out the positions from the position
  // buffer into the vertexPosition attribute.
  setPositionAttribute(gl, buffers, programInfo);

  // Tell WebGL to use our program when drawing
  gl.useProgram(programInfo.program);

  // Set the shader uniforms
  gl.uniformMatrix4fv(
    programInfo.uniformLocations.projectionMatrix,
    false,
    projectionMatrix,
  );
  gl.uniformMatrix4fv(
    programInfo.uniformLocations.modelViewMatrix,
    false,
    modelViewMatrix,
  );

  {
    const offset = 0;
    const vertexCount = 4;
    gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
  }
}

// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute.
function setPositionAttribute(gl, buffers, programInfo) {
  const numComponents = 2; // pull out 2 values per iteration
  const type = gl.FLOAT; // the data in the buffer is 32bit floats
  const normalize = false; // don't normalize
  const stride = 0; // how many bytes to get from one set of values to the next
  // 0 = use type and numComponents above
  const offset = 0; // how many bytes inside the buffer to start from
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexPosition,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}

export { drawScene };

Первый шаг — очистка холста до нашего цвета фона; затем мы устанавливаем перспективу камеры. Мы задаём поле обзора 45°, с соотношением ширины к высоте, соответствующим размерам отображения нашего холста. Мы также указываем, что хотим отрисовывать только объекты, находящиеся на расстоянии от 0,1 до 100 единиц от камеры.

Затем мы устанавливаем положение квадратной плоскости, загружая позицию по умолчанию и смещая её на 6 единиц от камеры. После этого, мы связываем буфер вершин квадрата с атрибутом, используемым шейдером для aVertexPosition, и указываем WebGL, как извлекать данные из него. Наконец, мы отрисовываем объект, вызывая метод drawArrays().

Наконец, давайте вызовем initBuffers() и drawScene().

Примечание: Добавьте этот код в начало файла "webgl-demo.js":

import { initBuffers } from "./init-buffers.js";
import { drawScene } from "./draw-scene.js";

Примечание: Добавьте этот код в конец вашей функции main():

// Here's where we call the routine that builds all the
// objects we'll be drawing.
const buffers = initBuffers(gl);

// Draw the scene
drawScene(gl, programInfo, buffers);

Результат должен выглядеть так:

Просмотреть весь код | Открыть эту демонстрацию в новой вкладке

Операции с матрицами

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

См. также

  • Матрицы на WebGLFundamentals
  • Матрицы на Wolfram MathWorld
  • Матрица на Википедии
  • Предыдущая
  • Следующая

© 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/Tutorial/Adding_2D_content_to_a_WebGL_context

Spec-Zone.ru

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