WebGL: что происходит под капотом

WebGL (Web Graphics Library) — это низкоуровневый API для отрисовки 2D- и 3D-графики в веб-браузере с использованием возможностей графического процессора (GPU). Он основан на спецификации OpenGL ES 2.0 и обеспечивает доступ к программируемому графическому конвейеру через JavaScript. Библиотека Three.js, в свою очередь, является высокоуровневой обёрткой над WebGL, скрывающей большую часть его сложности. Однако для понимания внутренней работы Three.js важно разобрать, как функционирует WebGL под капотом.

Контекст WebGL

Для начала необходимо получить WebGL-контекст из HTML-элемента <canvas>. Это объект, через который происходит взаимодействие JavaScript-кода с графическим API. Вызов getContext("webgl") или getContext("webgl2") возвращает объект, содержащий методы для работы с буферами, шейдерами, текстурами и другими графическими сущностями.

const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');

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

Программируемый графический конвейер

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

Вершинный шейдер (Vertex Shader)

Обрабатывает каждую вершину примитива. Задаёт её положение в пространстве и может передавать дополнительные данные на следующий этап.

Пример простейшего вершинного шейдера:

attribute vec3 position;

void main() {
    gl_Position = vec4(position, 1.0);
}

Фрагментный шейдер (Fragment Shader)

Отвечает за определение цвета каждого пикселя. Получает данные, интерполированные из вершин.

Пример фрагментного шейдера:

precision mediump float;

void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Красный цвет
}

Буферы и атрибуты

Для передачи данных в шейдеры используется механизм буферов. Наиболее распространённый — вершинный буфер (ARRAY_BUFFER), содержащий координаты вершин.

const vertices = new Float32Array([
    -1.0, -1.0, 0.0,
     1.0, -1.0, 0.0,
     0.0,  1.0, 0.0
]);

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

Затем буфер связывается с атрибутом в шейдере через gl.vertexAttribPointer() и gl.enableVertexAttribArray().

const positionLocation = gl.getAttribLocation(program, 'position');
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);

Униформы и управление состоянием

В отличие от атрибутов, uniform-переменные одинаковы для всех вершин или фрагментов в одном вызове отрисовки. Они задаются через JavaScript до начала рендера.

uniform mat4 modelViewProjectionMatrix;

Передача uniform в WebGL:

const matrixLocation = gl.getUniformLocation(program, 'modelViewProjectionMatrix');
gl.uniformMatrix4fv(matrixLocation, false, matrixData);

WebGL требует жесткого управления состоянием — привязки нужных буферов, программ, текстур и параметров перед каждым вызовом gl.drawArrays() или gl.drawElements().

Примитивы и отрисовка

После подготовки всех ресурсов вызывается один из методов отрисовки. Например:

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

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

В более сложных случаях используется индексированный рендеринг:

gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);

В этом случае данные индексов хранятся в отдельном буфере типа ELEMENT_ARRAY_BUFFER.

Текстуры

WebGL позволяет использовать текстуры для передачи изображений, данных освещения, теней и других эффектов. Текстура загружается, активируется и привязывается к определённому слоту:

const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// параметры и загрузка изображения
gl.texImage2D(...);

В GLSL-шейдерах текстура доступна через sampler2D, а чтение значений происходит с помощью texture2D() или texture():

uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
    gl_FragColor = texture2D(uTexture, vUv);
}

Глубина, альфа, смешивание

Для адекватной отрисовки трёхмерной сцены важно управление буфером глубины (Z-buffer). Он включает определение, какой объект ближе к камере, чтобы корректно отображать перекрытия.

gl.enable(gl.DEPTH_TEST);

Также используется альфа-смешивание для прозрачности:

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

Ограничения и оптимизация

WebGL — низкоуровневый API, и от разработчика требуется ручное управление:

  • Буферы должны быть обновлены вручную.
  • Шейдеры должны быть валидны, компилироваться и линковаться.
  • Текстуры должны быть повернуты, масштабированы и фильтрованы вручную.
  • Каждое изменение состояния (программа, текстура, буфер) потенциально влияет на производительность, поскольку приводит к обращениям к GPU.

По этой причине библиотеки, такие как Three.js, создают абстракции, которые автоматически управляют жизненным циклом объектов и оптимизируют отправку данных.

Связь с Three.js

Three.js генерирует WebGL-код на лету, компилирует шейдеры и управляет контекстом. При создании сцены, камеры, материалов и объектов Three.js транслирует эти сущности в WebGL-примитивы: создаёт буферы, компилирует шейдеры, загружает текстуры и запускает отрисовку.

Например, материал MeshStandardMaterial в Three.js под капотом создаёт набор uniform-переменных, передаёт световые параметры, генерирует GLSL-код, настраивает blending, depth и другие состояния, затем вызывает gl.drawElements() через собственный рендер-менеджер.

Понимание этих внутренних механизмов позволяет точнее управлять производительностью, корректно использовать материалы и камеры, и, при необходимости, внедрять собственные шейдеры через ShaderMaterial или RawShaderMaterial.

Вывод

WebGL предоставляет мощные, но низкоуровневые средства работы с графикой. Все высокоуровневые инструменты, включая Three.js, являются надстройками, которые управляют этими механизмами за разработчика. Глубокое понимание принципов WebGL необходимо при создании сложных визуальных эффектов, оптимизации производительности и использовании нестандартных рендер-процессов.