Добавление 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, тип шейдера и исходный код, а затем создаёт и компилирует шейдер следующим образом:
- Новый шейдер создаётся с помощью вызова
gl.createShader(). - Исходный код шейдера отправляется в шейдер с помощью вызова
gl.shaderSource(). - После того, как шейдер получил исходный код, он компилируется с помощью
gl.compileShader(). - Чтобы убедиться в успешной компиляции шейдера, проверяется параметр шейдера
gl.COMPILE_STATUS. Для получения его значения вызываетсяgl.getShaderParameter(), указывая шейдер и имя параметра, который мы хотим проверить (gl.COMPILE_STATUS). Если этоfalse, мы знаем, что компиляция шейдера завершилась неудачно, поэтому отображается всплывающее сообщение с информацией от компилятора, полученной с помощьюgl.getShaderInfoLog(), затем шейдер удаляется и возвращаетсяnullдля указания сбоя загрузки шейдера. - Если шейдер был загружен и успешно скомпилирован, скомпилированный шейдер возвращается вызывающей стороне.
Примечание: Добавьте этот код в вашу функцию 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.
См. также
© 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