Прежде всего, эти статьи посвящены WebGL2. Если вас интересует WebGL 1.0, пожалуйста, перейдите сюда. Обратите внимание, что WebGL2 почти на 100% обратно совместим с WebGL 1. Тем не менее, как только вы включите WebGL2, вам стоит использовать его так, как он был задуман. Эти туториалы следуют этому пути.
WebGL часто рассматривается как 3D API. Люди думают “Я использую WebGL и магия я получу крутую 3D графику”. В реальности WebGL - это просто движок растеризации. Он рисует точки, линии и треугольники на основе кода, который вы предоставляете. Заставить WebGL делать что-то еще - ваша задача предоставить код для использования точек, линий и треугольников для выполнения вашей задачи.
WebGL работает на GPU вашего компьютера. Как таковой, вам нужно предоставить код, который работает на этом GPU. Вы предоставляете этот код в виде пар функций. Эти 2 функции называются вершинным шейдером и фрагментным шейдером, и каждая из них написана на очень строго типизированном языке, похожем на C/C++, называемом GLSL. (GL Shader Language). Вместе они называются программой.
Задача вершинного шейдера - вычислять позиции вершин. На основе позиций, которые выводит функция, WebGL может затем растеризовать различные виды примитивов, включая точки, линии или треугольники. При растеризации этих примитивов он вызывает вторую пользовательскую функцию, называемую фрагментным шейдером. Задача фрагментного шейдера - вычислять цвет для каждого пикселя примитива, который в данный момент рисуется.
Почти весь WebGL API посвящен настройке состояния для выполнения этих пар функций.
Для каждой вещи, которую вы хотите нарисовать, вы настраиваете кучу состояния, а затем выполняете пару функций, вызывая
gl.drawArrays
или gl.drawElements
, который выполняет ваши шейдеры на GPU.
Любые данные, к которым вы хотите, чтобы эти функции имели доступ, должны быть предоставлены GPU. Есть 4 способа, как шейдер может получать данные.
Атрибуты, буферы и вершинные массивы
Буферы - это массивы бинарных данных, которые вы загружаете в GPU. Обычно буферы содержат такие вещи, как позиции, нормали, координаты текстуры, цвета вершин и т.д., хотя вы можете положить в них все, что хотите.
Атрибуты используются для указания того, как извлекать данные из ваших буферов и предоставлять их вашему вершинному шейдеру. Например, вы можете положить позиции в буфер как три 32-битных float’а на позицию. Вы бы сказали конкретному атрибуту, из какого буфера извлекать позиции, какой тип данных он должен извлекать (3 компонента 32-битных чисел с плавающей точкой), какое смещение в буфере начинаются позиции, и сколько байт нужно получить от одной позиции до следующей.
Буферы не являются случайным доступом. Вместо этого вершинный шейдер выполняется указанное количество раз. Каждый раз, когда он выполняется, извлекается следующее значение из каждого указанного буфера и присваивается атрибуту.
Состояние атрибутов, какие буферы использовать для каждого из них, и как извлекать данные из этих буферов собирается в объект вершинного массива (VAO).
Uniforms
Uniforms - это эффективно глобальные переменные, которые вы устанавливаете перед выполнением вашей шейдерной программы.
Текстуры
Текстуры - это массивы данных, к которым вы можете получить случайный доступ в вашей шейдерной программе. Самая распространенная вещь, которую кладут в текстуру - это данные изображения, но текстуры - это просто данные и могут так же легко содержать что-то другое, кроме цветов.
Varyings
Varyings - это способ для вершинного шейдера передать данные фрагментному шейдеру. В зависимости от того, что рендерится, точки, линии или треугольники, значения, установленные на varying вершинным шейдером, будут интерполированы при выполнении фрагментного шейдера.
WebGL заботится только о 2 вещах. Координаты clip space и цвета. Ваша задача как программиста, использующего WebGL, - предоставить WebGL эти 2 вещи. Вы предоставляете ваши 2 “шейдера” для этого. Вершинный шейдер, который предоставляет координаты clip space, и фрагментный шейдер, который предоставляет цвет.
Координаты clip space всегда идут от -1 до +1 независимо от размера вашего canvas. Вот простой пример WebGL, который показывает WebGL в его простейшей форме.
Давайте начнем с вершинного шейдера
#version 300 es
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
in vec4 a_position;
// все шейдеры имеют главную функцию
void main() {
// gl_Position - это специальная переменная, за установку которой
// отвечает вершинный шейдер
gl_Position = a_position;
}
При выполнении, если бы вся вещь была написана в JavaScript вместо GLSL, вы могли бы представить, что она использовалась бы так
// *** ПСЕВДО КОД!! ***
var positionBuffer = [
0, 0, 0, 0,
0, 0.5, 0, 0,
0.7, 0, 0, 0,
];
var attributes = {};
var gl_Position;
drawArrays(..., offset, count) {
var stride = 4;
var size = 4;
for (var i = 0; i < count; ++i) {
// копируем следующие 4 значения из positionBuffer в атрибут a_position
const start = offset + i * stride;
attributes.a_position = positionBuffer.slice(start, start + size);
runVertexShader();
...
doSomethingWith_gl_Position();
}
В реальности это не совсем так просто, потому что positionBuffer
нужно было бы преобразовать в бинарные
данные (см. ниже), и поэтому фактическое вычисление для получения данных из буфера
было бы немного другим, но надеюсь, это дает вам представление о том, как вершинный
шейдер будет выполняться.
Далее нам нужен фрагментный шейдер
#version 300 es
// фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно
// выбрать одну. highp - хороший выбор по умолчанию. Это означает "высокая точность"
precision highp float;
// нам нужно объявить выход для фрагментного шейдера
out vec4 outColor;
void main() {
// Просто устанавливаем выход на константный красно-фиолетовый
outColor = vec4(1, 0, 0.5, 1);
}
Выше мы объявили outColor
как выход нашего фрагментного шейдера. Мы устанавливаем outColor
в 1, 0, 0.5, 1
,
что означает 1 для красного, 0 для зеленого, 0.5 для синего, 1 для альфа. Цвета в WebGL идут от 0 до 1.
Теперь, когда мы написали 2 шейдерные функции, давайте начнем с WebGL
Сначала нам нужен HTML canvas элемент
<canvas id="c"></canvas>
Затем в JavaScript мы можем найти его
var canvas = document.querySelector("#c");
Теперь мы можем создать WebGL2RenderingContext
var gl = canvas.getContext("webgl2");
if (!gl) {
// нет webgl2 для вас!
...
Теперь нам нужно скомпилировать эти шейдеры, чтобы поместить их на GPU, поэтому сначала нам нужно получить их в строки. Вы можете создавать ваши GLSL строки любым способом, которым вы обычно создаете строки в JavaScript. Например, конкатенацией, используя AJAX для их загрузки, помещая их в не-javascript script теги, или в данном случае в многострочные шаблонные строки.
var vertexShaderSource = `#version 300 es
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
in vec4 a_position;
// все шейдеры имеют главную функцию
void main() {
// gl_Position - это специальная переменная, за установку которой
// отвечает вершинный шейдер
gl_Position = a_position;
}
`;
var fragmentShaderSource = `#version 300 es
// фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно
// выбрать одну. highp - хороший выбор по умолчанию. Это означает "высокая точность"
precision highp float;
// нам нужно объявить выход для фрагментного шейдера
out vec4 outColor;
void main() {
// Просто устанавливаем выход на константный красно-фиолетовый
outColor = vec4(1, 0, 0.5, 1);
}
`;
Фактически, большинство 3D движков генерируют GLSL шейдеры на лету, используя различные типы шаблонов, конкатенацию и т.д. Для примеров на этом сайте, однако, ни один из них не достаточно сложен, чтобы нуждаться в генерации GLSL во время выполнения.
ПРИМЕЧАНИЕ:
#version 300 es
ДОЛЖНА БЫТЬ САМОЙ ПЕРВОЙ СТРОКОЙ ВАШЕГО ШЕЙДЕРА. Никаких комментариев или пустых строк не допускается перед ней!#version 300 es
говорит WebGL2, что вы хотите использовать язык шейдеров WebGL2, называемый GLSL ES 3.00. Если вы не поставите это как первую строку, язык шейдеров по умолчанию будет использовать GLSL ES 1.00 WebGL 1.0, который имеет много различий и гораздо меньше функций.
Далее нам нужна функция, которая создаст шейдер, загрузит исходный код GLSL и скомпилирует шейдер. Обратите внимание, что я не написал никаких комментариев, потому что из названий функций должно быть ясно, что происходит.
function createShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
Теперь мы можем вызвать эту функцию для создания 2 шейдеров
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
Затем нам нужно связать эти 2 шейдера в программу
function createProgram(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
И вызвать её
var program = createProgram(gl, vertexShader, fragmentShader);
Теперь, когда мы создали программу GLSL на GPU, нам нужно предоставить ей данные.
Большая часть API WebGL посвящен настройке состояния для предоставления данных нашим программам GLSL.
В данном случае наш единственный ввод в программу GLSL - это a_position
, который является атрибутом.
Первое, что мы должны сделать, - это найти местоположение атрибута для программы,
которую мы только что создали
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
Поиск местоположений атрибутов (и uniform’ов) - это то, что вы должны делать во время инициализации, а не в цикле рендеринга.
Атрибуты получают свои данные из буферов, поэтому нам нужно создать буфер
var positionBuffer = gl.createBuffer();
WebGL позволяет нам манипулировать многими ресурсами WebGL на глобальных точках привязки. Вы можете думать о точках привязки как о внутренних глобальных переменных внутри WebGL. Сначала вы привязываете ресурс к точке привязки. Затем все остальные функции ссылаются на ресурс через точку привязки. Итак, давайте привяжем буфер позиций.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
Теперь мы можем поместить данные в этот буфер, ссылаясь на него через точку привязки
// три 2d точки
var positions = [
0, 0,
0, 0.5,
0.7, 0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
Здесь происходит много всего. Первое - у нас есть positions
, который является
массивом JavaScript. WebGL, с другой стороны, нужны строго типизированные данные, поэтому часть
new Float32Array(positions)
создает новый массив 32-битных чисел с плавающей точкой
и копирует значения из positions
. gl.bufferData
затем копирует эти данные в
positionBuffer
на GPU. Он использует буфер позиций, потому что мы привязали
его к точке привязки ARRAY_BUFFER
выше.
Последний аргумент, gl.STATIC_DRAW
, является подсказкой для WebGL о том, как мы будем использовать данные.
WebGL может попытаться использовать эту подсказку для оптимизации определенных вещей. gl.STATIC_DRAW
говорит WebGL,
что мы вряд ли будем часто изменять эти данные.
Теперь, когда мы поместили данные в буфер, нам нужно сказать атрибуту, как получать данные из него. Сначала нам нужно создать коллекцию состояния атрибутов, называемую Vertex Array Object.
var vao = gl.createVertexArray();
И нам нужно сделать это текущим массивом вершин, чтобы все наши настройки атрибутов применялись к этому набору состояния атрибутов
gl.bindVertexArray(vao);
Теперь мы наконец настраиваем атрибуты в массиве вершин. Сначала нам нужно включить атрибут. Это говорит WebGL, что мы хотим получать данные из буфера. Если мы не включим атрибут, то атрибут будет иметь постоянное значение.
gl.enableVertexAttribArray(positionAttributeLocation);
Затем нам нужно указать, как извлекать данные
var size = 2; // 2 компонента на итерацию
var type = gl.FLOAT; // данные - это 32-битные float'ы
var normalize = false; // не нормализовать данные
var stride = 0; // 0 = двигаться вперед на size * sizeof(type) каждый раз, чтобы получить следующую позицию
var offset = 0; // начать с начала буфера
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset)
Скрытая часть gl.vertexAttribPointer
заключается в том, что она привязывает текущий ARRAY_BUFFER
к атрибуту. Другими словами, теперь этот атрибут привязан к
positionBuffer
. Это означает, что мы свободны привязать что-то еще к точке привязки ARRAY_BUFFER
.
Атрибут продолжит использовать positionBuffer
.
Обратите внимание, что с точки зрения нашего GLSL вершинного шейдера атрибут a_position
является vec4
in vec4 a_position;
vec4
- это 4 значения float. В JavaScript вы могли бы думать об этом как о чем-то вроде
a_position = {x: 0, y: 0, z: 0, w: 0}
. Выше мы установили size = 2
. Атрибуты
по умолчанию равны 0, 0, 0, 1
, поэтому этот атрибут получит свои первые 2 значения (x и y)
из нашего буфера. z и w будут по умолчанию 0 и 1 соответственно.
Перед тем как рисовать, мы должны изменить размер холста, чтобы он соответствовал размеру отображения. Холсты, как и изображения, имеют 2 размера. Количество пикселей, фактически находящихся в них, и отдельно размер, в котором они отображаются. CSS определяет размер, в котором отображается холст. Вы всегда должны устанавливать размер, который вы хотите для холста, с помощью CSS, поскольку это намного более гибко, чем любой другой метод.
Чтобы количество пикселей в холсте соответствовало размеру, в котором он отображается, я использую вспомогательную функцию, о которой вы можете прочитать здесь.
Почти во всех этих примерах размер холста составляет 400x300 пикселей, если пример запущен в собственном окне, но растягивается, чтобы заполнить доступное пространство, если он находится внутри iframe, как на этой странице. Позволяя CSS определять размер, а затем настраивая соответствие, мы легко обрабатываем оба этих случая.
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
Нам нужно сказать WebGL, как конвертировать из значений clip space,
которые мы будем устанавливать в gl_Position
, обратно в пиксели, часто называемые screen space.
Для этого мы вызываем gl.viewport
и передаем ему текущий размер холста.
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
Это говорит WebGL, что clip space -1 +1 отображается на 0 <-> gl.canvas.width
для x и 0 <-> gl.canvas.height
для y.
Мы очищаем холст. 0, 0, 0, 0
- это красный, зеленый, синий, альфа соответственно, поэтому в данном случае мы делаем холст прозрачным.
// Очищаем холст
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
Далее нам нужно сказать WebGL, какую программу шейдеров выполнять.
// Говорим использовать нашу программу (пару шейдеров)
gl.useProgram(program);
Затем нам нужно сказать, какой набор буферов использовать и как извлекать данные из этих буферов для предоставления атрибутам
// Привязываем набор атрибутов/буферов, который мы хотим.
gl.bindVertexArray(vao);
После всего этого мы наконец можем попросить WebGL выполнить нашу программу GLSL.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
Поскольку count равен 3, это выполнит наш вершинный шейдер 3 раза. В первый раз a_position.x
и a_position.y
в нашем атрибуте вершинного шейдера будут установлены на первые 2 значения из positionBuffer.
Во второй раз a_position.xy
будет установлен на вторые два значения. В последний раз он будет
установлен на последние 2 значения.
Поскольку мы установили primitiveType
в gl.TRIANGLES
, каждый раз, когда наш вершинный шейдер запускается 3 раза,
WebGL нарисует треугольник на основе 3 значений, которые мы установили в gl_Position
. Неважно, какого размера
наш холст, эти значения находятся в координатах clip space, которые идут от -1 до 1 в каждом направлении.
Поскольку наш вершинный шейдер просто копирует значения positionBuffer в gl_Position
,
треугольник будет нарисован в координатах clip space
0, 0,
0, 0.5,
0.7, 0,
Конвертируя из clip space в screen space, если размер холста оказался 400x300, мы получили бы что-то вроде этого
clip space screen space
0, 0 -> 200, 150
0, 0.5 -> 200, 225
0.7, 0 -> 340, 150
WebGL теперь отрендерит этот треугольник. Для каждого пикселя, который он собирается нарисовать, WebGL вызовет наш фрагментный шейдер.
Наш фрагментный шейдер просто устанавливает outColor
в 1, 0, 0.5, 1
. Поскольку Canvas является 8-битным
на канал холстом, это означает, что WebGL собирается записать значения [255, 0, 127, 255]
в холст.
Вот живая версия
В случае выше вы можете видеть, что наш вершинный шейдер ничего не делает, кроме передачи наших данных позиции напрямую. Поскольку данные позиции уже в clip space, работы делать нечего. Если вы хотите 3D, вам нужно предоставить шейдеры, которые конвертируют из 3D в clip space, потому что WebGL - это только API растеризации.
Вы можете задаться вопросом, почему треугольник начинается в центре и идет к верхнему правому углу.
Clip space в x
идет от -1 до +1. Это означает, что 0 находится в центре, а положительные значения будут
справа от этого.
Что касается того, почему он находится сверху, в clip space -1 находится внизу, а +1 сверху. Это означает, что 0 находится в центре, и поэтому положительные числа будут выше центра.
Для 2D вещей вы, вероятно, предпочли бы работать в пикселях, чем в clip space, поэтому давайте изменим шейдер так, чтобы мы могли предоставить позицию в пикселях и иметь его конвертировать в clip space для нас. Вот новый вершинный шейдер
#version 300 es
// атрибут - это вход (in) в вершинный шейдер.
// Он будет получать данные из буфера
in vec2 a_position;
uniform vec2 u_resolution;
void main() {
// конвертируем позицию из пикселей в 0.0 до 1.0
vec2 zeroToOne = a_position / u_resolution;
// конвертируем из 0->1 в 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// конвертируем из 0->2 в -1->+1 (clip space)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace, 0, 1);
}
Некоторые вещи, которые стоит заметить об изменениях. Мы изменили a_position
на vec2
, поскольку мы
используем только x
и y
в любом случае. vec2
похож на vec4
, но имеет только x
и y
.
Далее мы добавили uniform
под названием u_resolution
. Чтобы установить это, нам нужно найти его местоположение.
var resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution");
Остальное должно быть ясно из комментариев. Устанавливая u_resolution
в разрешение
нашего холста, шейдер теперь будет принимать позиции, которые мы поместили в positionBuffer
, предоставленные
в координатах пикселей, и конвертировать их в clip space.
Теперь мы можем изменить наши значения позиции из clip space в пиксели. На этот раз мы собираемся нарисовать прямоугольник, сделанный из 2 треугольников, по 3 точки каждый.
var positions = [
10, 20,
80, 20,
10, 30,
10, 30,
80, 20,
80, 30,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
И после того, как мы установим, какую программу использовать, мы можем установить значение для uniform, который мы создали.
gl.useProgram
похож на gl.bindBuffer
выше в том, что он устанавливает текущую программу. После
этого все функции gl.uniformXXX
устанавливают uniforms на текущей программе.
gl.useProgram(program);
// Передаем разрешение холста, чтобы мы могли конвертировать из
// пикселей в clip space в шейдере
gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
И конечно, чтобы нарисовать 2 треугольника, нам нужно, чтобы WebGL вызвал наш вершинный шейдер 6 раз,
поэтому нам нужно изменить count
на 6
.
// рисуем
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6;
gl.drawArrays(primitiveType, offset, count);
И вот он
Примечание: Этот пример и все следующие примеры используют webgl-utils.js
,
который содержит функции для компиляции и связывания шейдеров. Нет причин загромождать примеры
этим boilerplate кодом.
Снова вы можете заметить, что прямоугольник находится рядом с нижней частью этой области. WebGL считает положительный Y вверх, а отрицательный Y вниз. В clip space левый нижний угол -1,-1. Мы не изменили никаких знаков, поэтому с нашей текущей математикой 0, 0 становится левым нижним углом. Чтобы получить более традиционный левый верхний угол, используемый для 2D графических API, мы можем просто перевернуть координату y clip space.
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
И теперь наш прямоугольник находится там, где мы ожидаем.
Давайте сделаем код, который определяет прямоугольник, функцией, чтобы мы могли вызывать её для прямоугольников разных размеров. Пока мы этим занимаемся, мы сделаем цвет настраиваемым.
Сначала мы делаем фрагментный шейдер принимающим uniform ввода цвета.
#version 300 es
precision highp float;
uniform vec4 u_color;
out vec4 outColor;
void main() {
outColor = u_color;
}
И вот новый код, который рисует 50 прямоугольников в случайных местах и случайных цветах.
var colorLocation = gl.getUniformLocation(program, "u_color");
...
// рисуем 50 случайных прямоугольников в случайных цветах
for (var ii = 0; ii < 50; ++ii) {
// Настраиваем случайный прямоугольник
setRectangle(
gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));
// Устанавливаем случайный цвет.
gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);
// Рисуем прямоугольник.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6;
gl.drawArrays(primitiveType, offset, count);
}
}
// Возвращает случайное целое число от 0 до range - 1.
function randomInt(range) {
return Math.floor(Math.random() * range);
}
// Заполняет буфер значениями, которые определяют прямоугольник.
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
// ПРИМЕЧАНИЕ: gl.bufferData(gl.ARRAY_BUFFER, ...) повлияет на
// любой буфер, привязанный к точке привязки `ARRAY_BUFFER`,
// но пока у нас только один буфер. Если бы у нас было больше одного
// буфера, мы бы хотели привязать этот буфер к `ARRAY_BUFFER` сначала.
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2]), gl.STATIC_DRAW);
}
И вот прямоугольники.
Я надеюсь, вы можете видеть, что WebGL на самом деле довольно простой API. Хорошо, простой может быть неправильным словом. То, что он делает, простое. Он просто выполняет 2 пользовательские функции, вершинный шейдер и фрагментный шейдер, и рисует треугольники, линии или точки. Хотя это может стать более сложным для 3D, эта сложность добавляется вами, программистом, в виде более сложных шейдеров. Сам API WebGL - это просто растеризатор и концептуально довольно прост.
Мы рассмотрели небольшой пример, который показал, как предоставлять данные в атрибуте и 2 uniforms. Обычно иметь несколько атрибутов и много uniforms. Ближе к началу этой статьи мы также упомянули varyings и текстуры. Они появятся в последующих уроках.
Прежде чем мы двинемся дальше, я хочу упомянуть, что для большинства приложений обновление
данных в буфере, как мы делали в setRectangle
, не является обычным. Я использовал этот
пример, потому что думал, что его легче всего объяснить, поскольку он показывает координаты пикселей
как ввод и демонстрирует выполнение небольшого количества математики в GLSL. Это не неправильно, есть
множество случаев, где это правильная вещь для делать, но вы должны продолжить читать, чтобы найти
более обычный способ позиционировать, ориентировать и масштабировать вещи в WebGL.
Если вы на 100% новичок в WebGL и не имеете представления о том, что такое GLSL или шейдеры или что делает GPU, тогда посмотрите основы того, как WebGL действительно работает. Вы также можете взглянуть на эту интерактивную диаграмму состояния для другого способа понимания того, как работает WebGL.
Вы также должны, по крайней мере кратко прочитать о boilerplate коде, используемом здесь, который используется в большинстве примеров. Вы также должны хотя бы бегло просмотреть как рисовать несколько вещей, чтобы дать вам некоторое представление о том, как структурированы более типичные WebGL приложения, потому что, к сожалению, почти все примеры рисуют только одну вещь и поэтому не показывают эту структуру.
Иначе отсюда вы можете пойти в 2 направлениях. Если вас интересует обработка изображений, я покажу вам как делать некоторую 2D обработку изображений. Если вас интересует изучение трансляции, вращения и масштабирования, тогда начните отсюда.