JavaScript WebGL API
Изучите JavaScript WebGL API: настройка контекста рендеринга, написание и компиляция шейдеров, загрузка вершинных буферов, рисование треугольника и использование uniform-переменных для 3D-графики.
Введение в WebGL
WebGL (Web Graphics Library, версия 1.0) использует возможности OpenGL ES 2.0 в веб-среде, позволяя разработчикам отображать детальную 3D-графику в любом совместимом браузере без необходимости в плагинах. Все примеры в этой главе используют WebGL 1.0 API. Эта возможность незаменима для создания иммерсивных игр, интерактивных 3D-приложений и сложных визуализаций прямо в браузере. Для современных проектов рекомендуется рассмотреть WebGL 2.0, основанный на OpenGL ES 3.0 и предлагающий улучшенную производительность и возможности.
В этой главе рассматривается всё необходимое для начала работы с WebGL: запрос контекста рендеринга, написание и компиляция шейдеров, загрузка вершинных данных в буферы и выполнение вызовов отрисовки. По итогу вы поймёте полный конвейер, стоящий за отображением одного треугольника, и узнаете, что изучать дальше: освещение, текстурирование и анимацию.
WebGL и 2D Canvas
WebGL использует тот же элемент <canvas>, что и 2D Canvas API, однако эти технологии кардинально отличаются. Контекст 2D (getContext('2d')) предоставляет высокоуровневую поверхность для рисования — fillRect, arc, drawImage. WebGL даёт низкоуровневый конвейер с аппаратным ускорением: вы описываете геометрию в виде массивов чисел и пишете небольшие программы (шейдеры), выполняющиеся на GPU и определяющие положение каждой вершины и цвет каждого пикселя. Этот дополнительный труд окупается аппаратным ускорением, настоящим 3D и пропускной способностью, необходимой для тысяч объектов на кадр.
Оба контекста запрашиваются одинаково, поэтому рекомендуется проверять поддержку:
const canvas = document.querySelector('#webglCanvas');
// 'webgl2' is preferred where available; fall back to 'webgl' (1.0).
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) {
console.error('WebGL is not supported by this browser.');
}Настройка первого контекста WebGL
Для начала работы с WebGL необходимо создать контекст рендеринга, привязанный к элементу canvas в вашем HTML. Для современных проектов можно запросить контекст WebGL 2 с помощью canvas.getContext('webgl2') для повышенной производительности и расширенных возможностей:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Simple WebGL Example</title>
<style>
canvas {
width: 400px;
height: 400px;
border: 1px solid black; /* Adds a border around the canvas */
}
</style>
</head>
<body>
<canvas id="webglCanvas"></canvas>
<script>
// This script will run once the DOM content is fully loaded.
document.addEventListener("DOMContentLoaded", function() {
// Get the canvas element.
const canvas = document.getElementById('webglCanvas');
// Initialize the WebGL 1.0 context.
const gl = canvas.getContext('webgl');
// Check if WebGL is available.
if (!gl) {
console.error('WebGL is not supported by your browser.');
return;
}
// Set the clear color to blue with full opacity.
gl.clearColor(0.0, 0.0, 1.0, 1.0); // RGBA: Blue color
// Clear the color buffer with the specified clear color.
gl.clear(gl.COLOR_BUFFER_BIT);
});
</script>
</body>
</html>Разбор кода
- Настройка HTML: HTML-часть создаёт элемент canvas, в котором WebGL будет отображать результат. Граница добавляется для визуального обозначения области canvas на странице.
- Стили CSS: Применяется простой стиль, задающий конкретный размер canvas и границу для наглядности.
- JavaScript для WebGL:
- Обработчик событий: Код JavaScript обёрнут в обработчик события, который ждёт полной загрузки DOM перед выполнением.
- Инициализация контекста WebGL: Получается контекст WebGL 1.0 из canvas. Если WebGL не поддерживается, контекст будет равен null.
- Проверка доступности WebGL: Если контекст равен null, в консоль выводится сообщение об ошибке, указывающее на отсутствие поддержки.
- Установка цвета очистки:
gl.clearColor(0.0, 0.0, 1.0, 1.0)задаёт цвет (синий, полностью непрозрачный), которым будет заполнен canvas при очистке цветового буфера. Обратите внимание: это лишь сохраняет цвет — ничего ещё не нарисовано. - Очистка цветового буфера:
gl.clear(gl.COLOR_BUFFER_BIT)фактически заливает canvas ранее установленным цветом очистки, создавая сплошной синий квадрат.
Этот пример является базовым, но даёт хорошую отправную точку для понимания того, как работает WebGL. Вы можете расширить его, добавив больше функций WebGL, таких как шейдеры, буферы и команды рисования для создания графических результатов.
Отрисовка простого треугольника
Один из первых шагов в изучении WebGL — отображение простых фигур. WebGL использует систему нормализованных координат устройства (NDC): видимая область находится в диапазоне от -1 до 1 по обеим осям X и Y, а точка (0, 0) находится в центре canvas. Каждая фигура должна быть описана в этих координатах (или преобразована в них с помощью шейдера).
Рисование даже одного треугольника требует полного конвейера WebGL:
- Написать вершинный шейдер, определяющий положение каждого угла.
- Написать фрагментный шейдер, задающий цвет каждого пикселя.
- Скомпилировать и скомпоновать их в программу шейдеров.
- Загрузить координаты углов в буфер.
- Подключить буфер к атрибуту шейдера и выполнить вызов отрисовки.
Пример ниже демонстрирует все пять шагов:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebGL Triangle Example</title>
<style>
canvas {
width: 400px;
height: 400px;
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="webglCanvas"></canvas>
<script>
// Function to create a shader, upload GLSL source code, and compile the shader
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Function to initialize the shader program
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
// Function to initialize WebGL
function initWebGL() {
const canvas = document.getElementById('webglCanvas');
// Note: Use 'webgl2' for modern projects
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGL is not supported by your browser.');
return;
}
// Set internal canvas resolution to match CSS dimensions
canvas.width = 400;
canvas.height = 400;
// Vertex shader program
const vsSource = `
attribute vec4 aVertexPosition;
void main(void) {
gl_Position = aVertexPosition;
}
`;
// Fragment shader program
const fsSource = `
void main(void) {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange color
}
`;
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition')
}
};
// Validate attribute location to prevent silent shader failures
if (programInfo.attribLocations.vertexPosition === -1) {
console.error('Failed to get the location of aVertexPosition');
return;
}
// Create a buffer for the triangle's positions.
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Set the positions for the triangle.
const positions = [
0.0, 1.0, // Vertex 1
-1.0, -1.0, // Vertex 2
1.0, -1.0 // Vertex 3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Draw the scene
function drawScene() {
// Note: High-DPI scaling is omitted for simplicity.
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clear(gl.COLOR_BUFFER_BIT);
// Tell WebGL to use our program when drawing
gl.useProgram(programInfo.program);
// Attach the position buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
2, // Number of components per vertex attribute
gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(
programInfo.attribLocations.vertexPosition);
// Execute WebGL program
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(drawScene);
}
drawScene();
}
// Call the initWebGL function after the document has loaded to ensure the canvas is ready.
document.addEventListener("DOMContentLoaded", initWebGL);
</script>
</body>
</html>Пояснение к коду
- Вершинный шейдер (
vsSource): Считывает атрибутaVertexPositionи присваивает его встроенной переменнойgl_Position, которая определяет положение каждой вершины на экране. - Фрагментный шейдер (
fsSource): Устанавливаетgl_FragColorтак, что каждый пиксель внутри треугольника отображается оранжевым (vec4(1.0, 0.5, 0.0, 1.0)— RGBA оранжевый). - Компиляция шейдера (
loadShader): Компилирует отдельный шейдер из исходного кода GLSL и сообщает об ошибках компиляции черезgetShaderInfoLog. - Инициализация программы шейдеров (
initShaderProgram): Компонует скомпилированные вершинный и фрагментный шейдеры в одну исполняемую программу, работающую на GPU. - Цикл анимации:
drawScene()выполняет вызов отрисовки, затемrequestAnimationFrame(drawScene)планирует следующий кадр, синхронизируя рендеринг с частотой обновления экрана. Для более глубокого изучения планирования кадров см. JavaScript-анимации.
Почему вызов отрисовки требует столько подготовки
Если вы привыкли к 2D Canvas API, весь этот шаблонный код может казаться излишним для одного треугольника. Причина в том, что WebGL сохраняет состояние и требует явных действий: ничего не рисуется, пока у вас нет (1) скомпонованной программы, (2) данных в буфере, (3) этого буфера, подключённого к атрибуту шейдера с помощью vertexAttribPointer, и (4) включённого enableVertexAttribArray. Пропустите любой шаг — и получите пустой canvas без ошибки, что является самой распространённой проблемой при работе с WebGL. Проверка attribLocations.vertexPosition === -1 в примере защищает от незаметно опечатанных имён атрибутов.
Работа с uniform-переменными
Атрибуты изменяются для каждой вершины; uniform-переменные остаются постоянными для всего вызова отрисовки и являются стандартным способом передачи изменяющихся значений — времени, цвета, матриц трансформации — из JavaScript в шейдер. Именно так можно анимировать или изменять цвет сцены без повторной загрузки геометрии каждый кадр.
Минимальный пример изменения цвета: объявите uniform-переменную во фрагментном шейдере, один раз получите её расположение, а затем обновляйте её каждый кадр.
// In the fragment shader source:
// precision mediump float;
// uniform vec4 uColor;
// void main(void) { gl_FragColor = uColor; }
const colorLocation = gl.getUniformLocation(shaderProgram, 'uColor');
function render(timeMs) {
const t = timeMs * 0.001; // seconds
const r = (Math.sin(t) + 1) / 2; // oscillate 0..1
gl.useProgram(shaderProgram);
gl.uniform4f(colorLocation, r, 0.5, 1.0 - r, 1.0); // RGBA
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
}
requestAnimationFrame(render);Поскольку геометрия не меняется, каждый кадр обновляется только uniform-переменная uColor — это значительно дешевле, чем перестраивать буферы.
Продвинутые техники в WebGL
По мере продвижения WebGL предлагает широкие возможности: освещение, текстурирование и управление геометрией:
- Текстуры позволяют накладывать изображения на поверхности с помощью
gl.texImage2Dи uniform-переменнойsampler2Dво фрагментном шейдере. - Индексные буферы (
gl.ELEMENT_ARRAY_BUFFER+gl.drawElements) повторно используют общие вершины, сокращая потребление памяти и стоимость отрисовки для сложных сеток. - Матрицы трансформации (модель/вид/проекция) переводят из плоских 2D-координат в настоящую 3D-перспективу; библиотеки, такие как glMatrix, берут на себя математические вычисления.
- Тест глубины (
gl.enable(gl.DEPTH_TEST)) обеспечивает правильное перекрытие дальних объектов ближними.
Для конкретных реализаций обратитесь к официальным примерам Khronos WebGL или к зрелой 3D-библиотеке, такой как Three.js, которая оборачивает низкоуровневый API в более удобный интерфейс.
Лучшие практики разработки на WebGL
- Проверка создания контекста: Всегда убеждайтесь, что
getContextвернул ненулевое значение, и предусмотрите корректный запасной вариант для браузеров без поддержки GPU. - Оптимизация производительности: Минимизируйте изменения состояния (
useProgram,bindBuffer), группируйте вызовы отрисовки и используйте индексную отрисовку для общих вершин. - Управление ресурсами GPU: Удаляйте ненужные буферы, текстуры и программы с помощью
gl.deleteBuffer,gl.deleteTextureиgl.deleteProgram, чтобы избежать утечек. - Кросс-браузерное тестирование: Убедитесь, что ваши WebGL-приложения работают стабильно в разных браузерах и на разных устройствах, и обрабатывайте редкое событие
webglcontextlost. - Взаимодействие с пользователем: Управляйте uniform-переменными из обработчиков событий ввода, чтобы сделать сцены динамичными — см. JavaScript-события для работы с пользовательским вводом.
Заключение
WebGL — мощный инструмент для веб-разработчиков, желающих интегрировать в свои приложения 3D-графику в реальном времени. При тщательном планировании и творческом подходе к реализации можно создавать впечатляющий визуальный опыт, работающий в браузерах без сбоев. Освоив WebGL с помощью подробных руководств и постоянной практики, вы откроете для себя новый горизонт возможностей в веб-разработке.