Если вы изучили HTML, CSS и JavaScript, возможно, вы подумаете: Могу ли я сделать что-то более интересное, чем отображение текста и изображений? Конечно, ДА! Например, вы можете использовать WebGL для создания интерактивной двухмерной или трехмерной графики.

Звучит сложно и вызывающе? Давайте начнем с чего-то простого: рисования 2D-фигуры.

Предварительные требования: обязательны HTML, CSS и JavaScript. Если вы знаете GLSL, это лучше, но я объясню код GLSL.

Репозиторий: https://github.com/yexiaosu/Intro-to-WebGL

Примечание. Чтобы увидеть результат, нельзя просто открыть HTML-файл. Вместо этого вам нужно запустить локальный сервер. Если вы используете VS Code, вы можете просто установить расширение под названием Live Server и нажать «Go Live» в нижней части окна VS Code.

Что такое WebGL?

WebGL (библиотека веб-графики) — это API-интерфейс JavaScript, полностью соответствующий OpenGL, который можно использовать в элементах HTML <canvas>, чтобы мы могли отображать 2D- или 3D-графику на холсте. WebGL может использовать аппаратное ускорение графики, предоставляемое устройством пользователя, что означает, что данные могут быть отправлены на графический процессор для рендеринга графики. ¹

Начало работы с WebGL

Как мы упоминали ранее, API используется в элементах HTML <canvas>, поэтому сначала нам нужен файл HTML с именем index.html с элементом <canvas>:

<!DOCTYPE html>
<head lang="en">
<meta charset="utf-8">
<title>Intro to WebGL</title>
<script src="webgl.js"></script>
</head>
<body>
<canvas width="300" height="300" style="border: solid 2px; padding: 20px;"></canvas>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

Здесь style просто используется, чтобы сделать холст более четким. Вы можете определить стиль холста по своему усмотрению, а также добавить дополнительные элементы в HTML-файл, если хотите. Файл JavaScript, который мы включили сюда, будет создан позже.

В этом уроке я нарисую вторичную отметку Мичиганского университета ².

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

Примечания:

  • Координаты в WebGL имеют диапазон от -1 до 1, поэтому нам нужно сделать некоторое преобразование;
  • Вы могли заметить, что я делю фигуру на несколько треугольников. Это связано с тем, что WebGL отображает фигуры, рисуя треугольники или полосу треугольников. Я выберу режим рисования треугольников, который будет проще при моделировании.

Поскольку мы смоделировали метку, мы можем сформировать информацию о ее геометрии. Информация о геометрии включает координаты вершин, цвет каждой вершины и порядок прохождения вершин при рисовании треугольников. Мы можем собрать эту информацию в файле JSON:

{
"attributes":
{"position":
[[0, 0.05]
,[0.4, 0.6]
,[1, 0.6]
,[1, 0.2]
,[0.85, 0.2]
,[0.85, -0.45]
,[1, -0.45]
,[1, -0.85]
,[0.25, -0.85]
,[0.25, -0.45]
,[0.4, -0.45]
,[0.4, -0.05]
,[0, -0.6]
,[-0.4, 0.6]
,[-1, 0.6]
,[-1, 0.2]
,[-0.85, 0.2]
,[-0.85, -0.45]
,[-1, -0.45]
,[-1, -0.85]
,[-0.25, -0.85]
,[-0.25, -0.45]
,[-0.4, -0.45]
,[-0.4, -0.05]
]
,"color":
[[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
,[1, 0.796, 0.0196, 1]
]
}
,"triangles":
[0,1,11
,0,11,12
,1,2,4
,2,3,4
,4,5,10
,1,4,10
,6,7,9
,7,8,9
,0,13,23
,0,23,12
,13,14,16
,14,15,16
,16,17,22
,13,16,22
,18,19,21
,19,20,21
]
}
view raw geometry.json hosted with ❤ by GitHub

Примечания:

  • Здесь я помещаю позиции и цвета в attributes, который отделен от triangles. Но вы можете определить структуру самостоятельно.
  • triangles — это список индексов вершин в списке position. Вы можете видеть, что каждые три индекса сгруппированы, что означает, что соответствующие три вершины являются вершинами одного треугольника.
  • Поскольку метка имеет только один цвет, каждая вершина имеет один и тот же цвет. Цвет указан в процентах RGBA. Когда WebGL визуализирует фигуру, он окрашивает каждую вершину, а цвета всех остальных пикселей вычисляются с использованием интерполяции ³.

Вершинный шейдер и фрагментный шейдер

Нам также нужно подготовить два шейдера для рендеринга формы с информацией о геометрии: вершинный шейдер и фрагментный шейдер. Как следует из названия, они имеют дело с вершинами и фрагментами между вершинами. Когда форма визуализируется:

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

Я знаю, что это сложно, если у вас раньше не было соответствующих знаний… Но не волнуйтесь! Мы здесь не делаем сложных вещей! У нас нет ни анимации, ни цветового градиента, ни освещения. Нам просто нужны два простых шейдера, чтобы использовать нашу информацию о геометрии для рендеринга статической 2D-формы:

#version 300 es
in vec4 position;
in vec4 color;
out vec4 vColor;
void main() {
vColor = color;
gl_Position = vec4(
position.xy,
position.zw
);
}
view raw vertex.glsl hosted with ❤ by GitHub
#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}
view raw fragment.glsl hosted with ❤ by GitHub

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

  • #version 300 es указывает версию языка GLSL.
  • precision highp float (во фрагментном шейдере) указывает точность типа float.
  • Ключевые слова in и out в GLSL определяют входные и выходные данные шейдера. vec4 объявляет 4-компонентный вектор.
  • В вершинном шейдере у нас есть два входа: position и color. position — координата вершины, а color — цвет этой вершины. Эти входные данные будут поступать из нашей геометрической информации.
  • В вершинном шейдере у нас также есть вывод с именем vColor. Это будет передано фрагментному шейдеру, в котором мы можем видеть ввод, также называемый vColor. В нашем примере, поскольку нам не нужно менять цвет, мы просто используем исходную информацию о цвете и передаем ее на выход фрагментного шейдера. Таким образом, каждый пиксель отображается как тот же цвет, что и все вершины.
  • В вершинном шейдере мы видим, что значение ввода position присваивается gl_Position. Это встроенная переменная, которая содержит информацию о положении, которая будет использоваться для визуализации вершины.

Более подробную информацию о GLSL можно найти в документе: OpenGL Wiki

Используйте API WebGL в JavaScript для рендеринга формы

Последний шаг — написать JavaScript! Давайте сначала создадим файл с именем webgl.js, который мы включили в наш файл HTML, и проверим файлы, которые у нас есть:

.
├── fragment.glsl
├── geometry.json
├── index.html
├── vertex.glsl
└── webgl.js

В webgl.js нам нужно сначала настроить среду WebGL и загрузить нужные нам ресурсы:

// Function to set up WebGL context and load resources
async function setup(event) {
// Get WebGL context
window.gl = document.querySelector("canvas").getContext("webgl2");
// Load resources
// Shaders
let vs = await fetch("vertex.glsl").then((res) => res.text());
let fs = await fetch("fragment.glsl").then((res) => res.text());
// Gerometry Information
let data = await fetch("geometry.json").then((r) => r.json());
}
// Add event listener to call setup function when page loads
window.addEventListener("load", setup);
view raw webgl.js hosted with ❤ by GitHub

Как вы могли заметить, на самом деле vs и fs являются строками. На самом деле шейдеры могут быть определены в файлах JavaScript в виде таких строк:

const fs = `
    #version 300 es
    precision highp float;
    
    in vec4 vColor;
    out vec4 fragColor;
    
    void main() {
       fragColor = vColor;
    }
`;

Но если шейдер сложный, делать это не рекомендуется.

После этого нам нужно скомпилировать шейдеры:

// Function to comile the shader and create shader object
function createShader(source, type) {
let shader = gl.createShader((type === "vertex" ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER));
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
throw Error(`${type} shader compilation failed`);
}
return shader;
}
view raw webgl.js hosted with ❤ by GitHub

Шейдеры также должны быть связаны с программой:

// Function to compile and link GLSL shaders
function compileAndLinkGLSL(vs_source, fs_source) {
// Create vertex shader object
vs = createShader(vs_source, "vertex");
// Create fragment shader object
fs = createShader(fs_source, "fragment");
// Create program object and attach shaders
window.program = gl.createProgram();
gl.attachShader(window.program, vs);
gl.attachShader(window.program, fs);
gl.linkProgram(window.program);
if (!gl.getProgramParameter(window.program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(window.program));
throw Error("Linking failed");
}
}
view raw webgl.js hosted with ❤ by GitHub

Давайте вызовем функцию compileAndLinkGLSL после загрузки исходников:

// Function to set up WebGL context and load resources
async function setup(event) {
// Get WebGL context and load resources
// ...
// Compile and link shaders
compileAndLinkGLSL(vs, fs);
}
view raw webgl.js hosted with ❤ by GitHub

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

// Function to set up geometry and bind attributes
function setupGeometry(geom) {
// Create vertex array object (VAO)
var triangleArray = gl.createVertexArray();
gl.bindVertexArray(triangleArray);
// Return object with VAO
return {
vao: triangleArray,
};
}
view raw webgl.js hosted with ❤ by GitHub

Обратите внимание, что gl.bindVertexArray() НЕ используется для привязки данных. Он фактически объявляет, какие VAO используются. Например, gl.bindVertexArray(triangleArray); означает, что мы используем triangleArray, а позже всякий раз, когда мы изменяем VAO, мы фактически изменяем triangleArray, пока не будет привязан другой VAO.

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

VAO на самом деле похож на :

// pseudo code
// reference: https://webglfundamentals.org/webgl/lessons/webgl-attributes.html
vertexArray = {
    attributes: [
      { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0, },
      // ... other attributes
    ],
    elementArrayBuffer: null,
}

Вот почему я поставил positions и colors в attributes из geometry.json. Фактически мы сохраняем их в буфере и присваиваем его атрибуту VAO, чтобы к буферам можно было получить доступ по расположению атрибутов.

Для этого мы можем обновить функцию setupGeometry():

// Function to set up geometry and bind attributes
function setupGeometry(geom) {
// Create vertex array object (VAO)
var triangleArray = gl.createVertexArray();
gl.bindVertexArray(triangleArray);
// Iterate over attributes and bind buffers
Object.entries(geom.attributes).forEach(([name, data]) => {
let buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
let f32 = new Float32Array(data.flat());
gl.bufferData(gl.ARRAY_BUFFER, f32, gl.STATIC_DRAW);
// Bind attribute location and enable vertex attrib array
let loc = gl.getAttribLocation(program, name);
gl.vertexAttribPointer(loc, data[0].length, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(loc);
});
// Return object with VAO
return {
vao: triangleArray,
};
}
view raw webgl.js hosted with ❤ by GitHub

Обратите внимание, что gl.bindBuffer() точно так же, как gl.bindVertexArray(), который только объявляет буфер, который будет использоваться позже. В то время как gl.bufferData() — это функция, используемая для загрузки данных в буфер.

Если вы хорошо разберетесь, вы можете вспомнить, что у нас также есть список triangle в нашей информации о геометрии, и мы не можем рисовать форму только с помощью position и color. Мы также должны создать буфер и сообщить WebGL, как рисовать треугольники. Вспомните псевдокод VAO, на этот раз мы будем использовать в нем elementArrayBuffer. Добавьте следующий код после того, как мы переберем атрибуты и привяжем буферы:

// Function to set up geometry and bind attributes
function setupGeometry(geom) {
// Create vertex array object (VAO) and iterate over attributes and bind buffers
// ...
// Create index buffer
var indices = new Uint16Array(geom.triangles.flat());
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// Return object with VAO, mode, count, and type properties
return {
mode: gl.TRIANGLES,
count: indices.length,
type: gl.UNSIGNED_SHORT,
vao: triangleArray,
};
}
view raw webgl.js hosted with ❤ by GitHub

Обратите внимание, что на этот раз, когда мы связываем буфер, мы используем gl.ELEMENT_ARRAY_BUFFER вместо gl.ARRAY_BUFFER в качестве точки привязки, которая всегда используется для индексов. Кроме того, мы добавляем некоторую другую информацию к возвращаемому объекту. Эта информация будет использоваться позже, чтобы указать GPU, как считывать данные из буферов при рендеринге фигур.

После этого давайте вызовем функцию setupGeometry() после компиляции и компоновки шейдеров. Чтобы использовать информацию и VAO позже, мы можем присвоить объект, возвращаемый этой функцией, переменной окна:

// Function to set up WebGL context and load resources
async function setup(event) {
// Get WebGL context and load resources
// ...
// Compile and link shaders, set up geometry
compileAndLinkGLSL(vs, fs);
window.geom = setupGeometry(data);
}
view raw webgl.js hosted with ❤ by GitHub

Последний шаг — рисование, которое очень просто. Все, что нам нужно сделать, это использовать нашу программу и вызвать функцию gl.drawElements, предоставляемую WebGL:

// Function to draw geometry
function draw() {
// Declare which program to be used
gl.useProgram(window.program);
// Bind vertex array object and draw elements
gl.bindVertexArray(window.geom.vao);
gl.drawElements(window.geom.mode, window.geom.count, window.geom.type, 0);
}
view raw webgl.js hosted with ❤ by GitHub

И вызовите функцию в функции setup():

// Function to set up WebGL context and load resources
async function setup(event) {
// Get WebGL context and load resources
// ...
// Compile and link shaders, set up geometry, and draw
compileAndLinkGLSL(vs, fs);
window.geom = setupGeometry(data);
draw();
}
view raw webgl.js hosted with ❤ by GitHub

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

Ссылка

[1] WebGL: 2D- и 3D-графика для Интернета — веб-API: MDN. веб-API | МДН. (н.д.). Получено 19 апреля 2023 г. с https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API.

[2] Рекомендации по использованию логотипа UM. Вице-президент по коммуникациям Мичиганского университета. (н.д.). Получено 19 апреля 2023 г. с https://brand.umich.edu/logos/u-m-logo/.

[3] Использование шейдеров для применения цвета в webgl — веб-API: MDN. веб-API | МДН. (н.д.). Получено 20 апреля 2023 г. с https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_shaders_to_apply_color_in_WebGL.

[4] Язык затенения OpenGL. Язык шейдеров OpenGL — OpenGL Wiki. (н.д.). Получено 20 апреля 2023 г. с https://khronos.org/opengl/wiki/OpenGL_Shading_Language.

[5] Атрибуты Webgl. Атрибуты WebGL. (н.д.). Получено 20 апреля 2023 г. с https://webglfundamentals.org/webgl/lessons/webgl-attributes.html.

[6] Индексированные вершины WebGL. Индексированные вершины WebGL. (н.д.). Получено 20 апреля 2023 г. с https://webglfundamentals.org/webgl/lessons/webgl-indexed-vertices.html.