diff --git a/webgl/lessons/ru/webgl-2d-matrices.md b/webgl/lessons/ru/webgl-2d-matrices.md index d66886f0b..367819bdf 100644 --- a/webgl/lessons/ru/webgl-2d-matrices.md +++ b/webgl/lessons/ru/webgl-2d-matrices.md @@ -120,7 +120,7 @@ c = Math.cos(angleToRotateInRadians);
- +
newX = x * c +newY = x * -s +extra = x * 0.0 +
y * s +y * c + y * 0.0 +
y * s +y * c + y * 0.0 +
1 * 0.0 1 * 0.0  1 * 1.0 
И упрощая, мы получаем @@ -151,7 +151,7 @@ newY = x * -s + y * c;
- +
newX = x * sx +newY = x * 0.0 +extra = x * 0.0 +
y * 0.0 +y * sy + y * 0.0 +
y * 0.0 +y * sy + y * 0.0 +
1 * 0.0 1 * 0.0  1 * 1.0 
что упрощенно @@ -198,4 +198,516 @@ var m3 = { b00 * a01 + b01 * a11 + b02 * a21, b00 * a02 + b01 * a12 + b02 * a22, b10 * a00 + b11 * a10 + b12 * a20, -``` \ No newline at end of file + b10 * a01 + b11 * a11 + b12 * a21, + b10 * a02 + b11 * a12 + b12 * a22, + b20 * a00 + b21 * a10 + b22 * a20, + b20 * a01 + b21 * a11 + b22 * a21, + b20 * a02 + b21 * a12 + b22 * a22, + ]; + } +} +``` + +Чтобы сделать вещи более ясными, давайте создадим функции для построения матриц для +перемещения, поворота и масштабирования. + + var m3 = { + translation: function(tx, ty) { + return [ + 1, 0, 0, + 0, 1, 0, + tx, ty, 1, + ]; + }, + + rotation: function(angleInRadians) { + var c = Math.cos(angleInRadians); + var s = Math.sin(angleInRadians); + return [ + c,-s, 0, + s, c, 0, + 0, 0, 1, + ]; + }, + + scaling: function(sx, sy) { + return [ + sx, 0, 0, + 0, sy, 0, + 0, 0, 1, + ]; + }, + }; + +Теперь давайте изменим наш шейдер. Старый шейдер выглядел так + +```glsl +#version 300 es + +in vec2 a_position; + +uniform vec2 u_resolution; +uniform vec2 u_translation; +uniform vec2 u_rotation; +uniform vec2 u_scale; + +void main() { + // Масштабируем позицию + vec2 scaledPosition = a_position * u_scale; + + // Поворачиваем позицию + vec2 rotatedPosition = vec2( + scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x, + scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x); + + // Добавляем перемещение. + vec2 position = rotatedPosition + u_translation; +``` + +Наш новый шейдер будет намного проще. + +```glsl +#version 300 es + +in vec2 a_position; + +uniform vec2 u_resolution; +uniform mat3 u_matrix; + +void main() { + // Умножаем позицию на матрицу. + vec2 position = (u_matrix * vec3(a_position, 1)).xy; + ... +``` + +И вот как мы его используем + +```js + // Рисуем сцену. + function drawScene() { + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + // Говорим WebGL, как конвертировать из clip space в пиксели + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Очищаем холст + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // Говорим использовать нашу программу (пару шейдеров) + gl.useProgram(program); + + // Привязываем набор атрибутов/буферов, который мы хотим. + gl.bindVertexArray(vao); + + // Вычисляем матрицы + var projectionMatrix = m3.projection( + gl.canvas.clientWidth, gl.canvas.clientHeight); + var translationMatrix = m3.translation(translation[0], translation[1]); + var rotationMatrix = m3.rotation(rotationInRadians); + var scaleMatrix = m3.scaling(scale[0], scale[1]); + + // Умножаем матрицы. + var matrix = m3.multiply(projectionMatrix, translationMatrix); + matrix = m3.multiply(matrix, rotationMatrix); + matrix = m3.multiply(matrix, scaleMatrix); + + // Устанавливаем матрицу. + gl.uniformMatrix3fv(matrixLocation, false, matrix); + + // Устанавливаем цвет. + gl.uniform4fv(colorLocation, color); + + // Рисуем прямоугольник. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 18; + gl.drawArrays(primitiveType, offset, count); + } +``` + +Вот пример использования нашего нового кода. Слайдеры те же, перемещение, +поворот и масштабирование. Но способ их использования в шейдере намного проще. + +{{{example url="../webgl-2d-geometry-matrix-transform.html" }}} + +Все еще, вы можете спрашивать, и что? Это не кажется большой выгодой. +Но теперь, если мы хотим изменить порядок, нам не нужно писать новый шейдер. +Мы можем просто изменить математику. + + ... + // Умножаем матрицы. + var matrix = m3.multiply(scaleMatrix, rotationMatrix); + matrix = m3.multiply(matrix, translationMatrix); + ... + +Вот эта версия. + +{{{example url="../webgl-2d-geometry-matrix-transform-trs.html" }}} + +Возможность применять матрицы таким образом особенно важна для +иерархической анимации, как руки на теле, луны на планете вокруг +солнца, или ветви на дереве. Для простого примера иерархической +анимации давайте нарисуем нашу 'F' 5 раз, но каждый раз давайте начнем с +матрицы от предыдущей 'F'. + +```js + // Рисуем сцену. + function drawScene() { + + ... + + // Вычисляем матрицы + var translationMatrix = m3.translation(translation[0], translation[1]); + var rotationMatrix = m3.rotation(rotationInRadians); + var scaleMatrix = m3.scaling(scale[0], scale[1]); + + // Начальная матрица. + var matrix = m3.identity(); + + for (var i = 0; i < 5; ++i) { + // Умножаем матрицы. + matrix = m3.multiply(matrix, translationMatrix); + matrix = m3.multiply(matrix, rotationMatrix); + matrix = m3.multiply(matrix, scaleMatrix); + + // Устанавливаем матрицу. + gl.uniformMatrix3fv(matrixLocation, false, matrix); + + // Рисуем геометрию. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 18; + gl.drawArrays(primitiveType, offset, count); + } + } +``` + +Для этого мы ввели функцию `m3.identity`, которая создает +единичную матрицу. Единичная матрица - это матрица, которая эффективно представляет +1.0, так что если вы умножаете на единичную матрицу, ничего не происходит. Так же как + +
X * 1 = X
+ +так и + +
matrixX * identity = matrixX
+ +Вот код для создания единичной матрицы. + + var m3 = { + identity: function () { + return [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + ]; + }, + }; + +Вот 5 F. + +{{{example url="../webgl-2d-geometry-matrix-transform-hierarchical.html" }}} + +Давайте посмотрим еще один пример. Во всех примерах до сих пор наша 'F' вращается вокруг своего +верхнего левого угла. Это потому, что математика, которую мы используем, всегда вращается вокруг +начала координат, а верхний левый угол нашей 'F' находится в начале координат, (0, 0). + +Но теперь, поскольку мы можем делать матричную математику и можем выбирать порядок, в котором +применяются преобразования, мы можем эффективно переместить начало координат перед тем, как остальные +преобразования будут применены. + +```js + // создаем матрицу, которая переместит начало координат 'F' в его центр. + var moveOriginMatrix = m3.translation(-50, -75); + ... + + // Умножаем матрицы. + var matrix = m3.multiply(translationMatrix, rotationMatrix); + matrix = m3.multiply(matrix, scaleMatrix); + matrix = m3.multiply(matrix, moveOriginMatrix); +``` + +Вот этот пример. Обратите внимание, что F вращается и масштабируется вокруг центра. + +{{{example url="../webgl-2d-geometry-matrix-transform-center-f.html" }}} + +Используя эту технику, вы можете вращать или масштабировать из любой точки. Теперь вы знаете, +как Photoshop или Flash позволяют вам перемещать точку вращения какого-то изображения. + +Давайте пойдем еще дальше. Если вы вернетесь к первой статье о +[основах WebGL](webgl-fundamentals.html), вы можете вспомнить, что у нас есть код +в шейдере для конвертации из пикселей в clip space, который выглядит так. + + ... + // конвертируем прямоугольник из пикселей в 0.0 до 1.0 + vec2 zeroToOne = 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 * vec2(1, -1), 0, 1); + +Если вы посмотрите на каждый из этих шагов по очереди, первый шаг, +"конвертируем из пикселей в 0.0 до 1.0", на самом деле является операцией масштабирования. +Второй также является операцией масштабирования. Следующий - это перемещение, +и самый последний масштабирует Y на -1. Мы можем на самом деле сделать все это в +матрице, которую мы передаем в шейдер. Мы могли бы создать 2 матрицы масштабирования, +одну для масштабирования на 1.0/resolution, другую для масштабирования на 2.0, третью для +перемещения на -1.0,-1.0 и четвертую для масштабирования Y на -1, затем умножить +их все вместе, но вместо этого, поскольку математика простая, +мы просто создадим функцию, которая создает матрицу 'проекции' для +заданного разрешения напрямую. + + var m3 = { + projection: function (width, height) { + // Примечание: Эта матрица переворачивает ось Y так, что 0 находится сверху. + return [ + 2 / width, 0, 0, + 0, -2 / height, 0, + -1, 1, 1, + ]; + }, + ... + +Теперь мы можем упростить шейдер еще больше. Вот весь новый вершинный шейдер. + + #version 300 es + + in vec2 a_position; + + uniform mat3 u_matrix; + + void main() { + // Умножаем позицию на матрицу. + gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); + } + +И в JavaScript нам нужно умножить на матрицу проекции + +```js + // Рисуем сцену. + function drawScene() { + ... +- // Передаем разрешение холста, чтобы мы могли конвертировать из +- // пикселей в clip space в шейдере +- gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); + + ... + + // Вычисляем матрицы + var projectionMatrix = m3.projection( + gl.canvas.clientWidth, gl.canvas.clientHeight); + var translationMatrix = m3.translation(translation[0], translation[1]); + var rotationMatrix = m3.rotation(rotationInRadians); + var scaleMatrix = m3.scaling(scale[0], scale[1]); + + // Умножаем матрицы. +* var matrix = m3.multiply(projectionMatrix, translationMatrix); +* matrix = m3.multiply(matrix, rotationMatrix); + matrix = m3.multiply(matrix, scaleMatrix); + ... + } +``` + +Мы также удалили код, который устанавливал разрешение. С этим последним шагом мы перешли +от довольно сложного шейдера с 6-7 шагами к очень простому шейдеру только с +1 шагом, все благодаря магии матричной математики. + +{{{example url="../webgl-2d-geometry-matrix-transform-with-projection.html" }}} + +Прежде чем мы продолжим, давайте немного упростим. Хотя обычно генерировать +различные матрицы и отдельно умножать их вместе, также обычно просто +умножать их по ходу дела. Эффективно мы могли бы функции как эти + +```js +var m3 = { + + ... + + translate: function(m, tx, ty) { + return m3.multiply(m, m3.translation(tx, ty)); + }, + + rotate: function(m, angleInRadians) { + return m3.multiply(m, m3.rotation(angleInRadians)); + }, + + scale: function(m, sx, sy) { + return m3.multiply(m, m3.scaling(sx, sy)); + }, + + ... + +}; +``` + +Это позволило бы нам изменить 7 строк матричного кода выше на всего 4 строки, как эти + +```js +// Вычисляем матрицу +var matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight); +matrix = m3.translate(matrix, translation[0], translation[1]); +matrix = m3.rotate(matrix, rotationInRadians); +matrix = m3.scale(matrix, scale[0], scale[1]); +``` + +И вот это + +{{{example url="../webgl-2d-geometry-matrix-transform-simpler-functions.html" }}} + +Последняя вещь, мы видели выше, что порядок имеет значение. В первом примере у нас было + + translation * rotation * scale + +а во втором у нас было + + scale * rotation * translation + +И мы видели, как они разные. + +Есть 2 способа смотреть на матрицы. Учитывая выражение + + projectionMat * translationMat * rotationMat * scaleMat * position + +Первый способ, который многие люди находят естественным, - начать справа и работать +влево + +Сначала мы умножаем позицию на матрицу масштабирования, чтобы получить масштабированную позицию + + scaledPosition = scaleMat * position + +Затем мы умножаем scaledPosition на матрицу поворота, чтобы получить rotatedScaledPosition + + rotatedScaledPosition = rotationMat * scaledPosition + +Затем мы умножаем rotatedScaledPosition на матрицу перемещения, чтобы получить +translatedRotatedScaledPosition + + translatedRotatedScaledPosition = translationMat * rotatedScaledPosition + +И наконец мы умножаем это на матрицу проекции, чтобы получить позиции в clip space + + clipspacePosition = projectionMatrix * translatedRotatedScaledPosition + +Второй способ смотреть на матрицы - читать слева направо. В этом случае +каждая матрица изменяет "пространство", представленное холстом. Холст начинается +с представления clip space (-1 до +1) в каждом направлении. Каждая примененная матрица +слева направо изменяет пространство, представленное холстом. + +Шаг 1: нет матрицы (или единичная матрица) + +> {{{diagram url="resources/matrix-space-change.html?stage=0" caption="clip space" }}} +> +> Белая область - это холст. Синий - вне холста. Мы в clip space. +> Передаваемые позиции должны быть в clip space + +Шаг 2: `matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight)`; + +> {{{diagram url="resources/matrix-space-change.html?stage=1" caption="из clip space в pixel space" }}} +> +> Теперь мы в pixel space. X = 0 до 400, Y = 0 до 300 с 0,0 в верхнем левом углу. +> Позиции, передаваемые с использованием этой матрицы, должны быть в pixel space. Вспышка, которую вы видите, +> это когда пространство переворачивается с положительного Y = вверх на положительный Y = вниз. + +Шаг 3: `matrix = m3.translate(matrix, tx, ty);` + +> {{{diagram url="resources/matrix-space-change.html?stage=2" caption="переместить начало координат в tx, ty" }}} +> +> Начало координат теперь перемещено в tx, ty (150, 100). Пространство переместилось. + +Шаг 4: `matrix = m3.rotate(matrix, rotationInRadians);` + +> {{{diagram url="resources/matrix-space-change.html?stage=3" caption="повернуть на 33 градуса" }}} +> +> Пространство повернуто вокруг tx, ty + +Шаг 5: `matrix = m3.scale(matrix, sx, sy);` + +> {{{diagram url="resources/matrix-space-change.html?stage=4" capture="масштабировать пространство" }}} +> +> Предварительно повернутое пространство с центром в tx, ty масштабировано на 2 по x, 1.5 по y + +В шейдере мы затем делаем `gl_Position = matrix * position;`. Значения `position` эффективно находятся в этом финальном пространстве. + +Используйте тот способ, который вам легче понять. + +Я надеюсь, что эти посты помогли развеять тайну матричной математики. Если вы хотите +остаться с 2D, я бы предложил проверить [воссоздание функции drawImage canvas 2d](webgl-2d-drawimage.html) и следовать за этим +в [воссоздание матричного стека canvas 2d](webgl-2d-matrix-stack.html). + +Иначе дальше [мы перейдем к 3D](webgl-3d-orthographic.html). +В 3D матричная математика следует тем же принципам и использованию. +Я начал с 2D, чтобы надеюсь сохранить это простым для понимания. + +Также, если вы действительно хотите стать экспертом +в матричной математике [проверьте эти удивительные видео](https://www.youtube.com/watch?v=kjBOesZCoqc&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab). + +
+

Что такое clientWidth и clientHeight?

+

До этого момента, когда я ссылался на размеры холста, +я использовал canvas.width и canvas.height, +но выше, когда я вызывал m3.projection, я вместо этого использовал +canvas.clientWidth и canvas.clientHeight. +Почему?

+

Матрицы проекции касаются того, как взять clip space +(-1 до +1 в каждом измерении) и конвертировать его обратно +в пиксели. Но в браузере есть 2 типа пикселей, с которыми мы +имеем дело. Один - это количество пикселей в +самом холсте. Так, например, холст, определенный так.

+
+  <canvas width="400" height="300"></canvas>
+
+

или один, определенный так

+
+  var canvas = document.createElement("canvas");
+  canvas.width = 400;
+  canvas.height = 300;
+
+

оба содержат изображение 400 пикселей в ширину на 300 пикселей в высоту. +Но этот размер отделен от того размера, +который браузер фактически отображает этот 400x300 пиксельный холст. +CSS определяет, какого размера отображается холст. +Например, если мы сделали холст так.

+

+  <style>
+  canvas {
+    width: 100%;
+    height: 100%;
+  }
+  </style>
+  ...
+  <canvas width="400" height="300"></canvas>
+
+

Холст будет отображаться любого размера, каким является его контейнер. +Это, вероятно, не 400x300.

+

Вот два примера, которые устанавливают CSS размер отображения холста на +100%, так что холст растягивается, +чтобы заполнить страницу. Первый использует canvas.widthcanvas.height. Откройте его в новом +окне и измените размер окна. Обратите внимание, как 'F' +не имеет правильного соотношения сторон. Она искажается.

+{{{example url="../webgl-canvas-width-height.html" width="500" height="150" }}} +

В этом втором примере мы используем canvas.clientWidthcanvas.clientHeight. canvas.clientWidthcanvas.clientHeight сообщают +размер, который холст фактически отображается браузером, так что +в этом случае, даже хотя холст все еще имеет только 400x300 пикселей, +поскольку мы определяем наше соотношение сторон на основе размера, который холст +отображается, F всегда выглядит правильно.

+{{{example url="../webgl-canvas-clientwidth-clientheight.html" width="500" height="150" }}} +

Большинство приложений, которые позволяют изменять размер их холстов, пытаются сделать +canvas.width и canvas.height соответствующими +canvas.clientWidth и canvas.clientHeight, +потому что они хотят, чтобы был +один пиксель в холсте для каждого пикселя, отображаемого браузером. +Но, как мы видели выше, это не +единственный вариант. Это означает, что почти во всех случаях более +технически правильно вычислять +соотношение сторон матрицы проекции, используя canvas.clientHeightcanvas.clientWidth. Тогда вы получите правильное соотношение сторон +независимо от того, соответствуют ли ширина и высота холста +размеру, который браузер рисует холст. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-drawing-without-data.md b/webgl/lessons/ru/webgl-drawing-without-data.md index cbd3e0ae8..12953879e 100644 --- a/webgl/lessons/ru/webgl-drawing-without-data.md +++ b/webgl/lessons/ru/webgl-drawing-without-data.md @@ -196,4 +196,360 @@ function render(time) { const offset = 0; gl.drawArrays(gl.POINTS, offset, numVerts); -} \ No newline at end of file +} +``` + +{{{example url="../webgl-no-data-point-rain-linear.html"}}} + +Это дает нам POINTS, идущие вниз по экрану, но они все +в порядке. Нам нужно добавить некоторую случайность. В GLSL нет +генератора случайных чисел. Вместо этого мы можем использовать +функцию, которая генерирует что-то, что кажется достаточно случайным. + +Вот одна + +```glsl +// hash функция из https://www.shadertoy.com/view/4djSRW +// дано значение между 0 и 1 +// возвращает значение между 0 и 1, которое *выглядит* довольно случайным +float hash(float p) { + vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); + p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); + return fract(p2.x * p2.y * 95.4337); +} +``` + +и мы можем использовать это так + +```glsl +void main() { + float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 + float x = hash(u) * 2.0 - 1.0; // случайная позиция + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 2.0; +} +``` + +Мы передаем `hash` наше предыдущее значение от 0 до 1, и оно дает нам +обратно псевдослучайное значение от 0 до 1. + +Давайте также сделаем точки меньше + +```glsl + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 2.0; +``` + +И увеличим количество точек, которые мы рисуем + +```js +const numVerts = 400; +``` + +И с этим мы получаем + +{{{example url="../webgl-no-data-point-rain.html"}}} + +Если вы посмотрите очень внимательно, вы можете увидеть, что дождь повторяется. +Ищите какую-то группу точек и смотрите, как они падают с +низа и появляются обратно сверху. +Если бы на заднем плане происходило больше, как если бы +этот дешевый эффект дождя происходил поверх 3D игры, +возможно, никто никогда не заметил бы, что он повторяется. + +Мы можем исправить повторение, добавив немного больше случайности. + +```glsl +void main() { + float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 + float off = floor(time + u) / 1000.0; // изменяется раз в секунду на вершину + float x = hash(u + off) * 2.0 - 1.0; // случайная позиция + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 2.0; +} +``` + +В коде выше мы добавили `off`. Поскольку мы вызываем `floor`, +значение `floor(time + u)` будет эффективно давать нам +секундный таймер, который изменяется только раз в секунду для каждой вершины. +Это смещение синхронизировано с кодом, перемещающим точку вниз по экрану, +так что в тот же момент, когда точка прыгает обратно наверх +экрана, добавляется небольшое количество к значению, +которое передается в `hash`, что означает, что эта конкретная точка +получит новое случайное число и, следовательно, новую случайную горизонтальную позицию. + +Результат - эффект дождя, который не кажется повторяющимся + +{{{example url="../webgl-no-data-point-rain-less-repeat.html"}}} + +Можем ли мы делать больше, чем `gl.POINTS`? Конечно! + +Давайте сделаем круги. Для этого нам нужны треугольники вокруг +центра, как ломтики пирога. Мы можем думать о каждом треугольнике +как о 2 точках вокруг края пирога, за которыми следует 1 точка в центре. +Затем мы повторяем для каждого ломтика пирога. + +

+ +Итак, сначала мы хотим какой-то счетчик, который изменяется раз на ломтик пирога + +```glsl +int sliceId = gl_VertexID / 3; +``` + +Затем нам нужен счет вокруг края круга, который идет + + 0, 1, ?, 1, 2, ?, 2, 3, ?, ... + +Значение ? не имеет значения, потому что, глядя на +диаграмму выше, 3-е значение всегда в центре (0,0), +так что мы можем просто умножить на 0 независимо от значения. + +Чтобы получить паттерн выше, это сработает + +```glsl +int triVertexId = gl_VertexID % 3; +int edge = triVertexId + sliceId; +``` + +Для точек на краю против точек в центре нам нужен +этот паттерн. 2 на краю, затем 1 в центре, повторять. + + 1, 1, 0, 1, 1, 0, 1, 1, 0, ... + +Мы можем получить этот паттерн с + +```glsl +float radius = step(1.5, float(triVertexId)); +``` + +`step(a, b)` это 0, если a < b, и 1 в противном случае. Вы можете думать об этом как + +```js +function step(a, b) { + return a < b ? 0 : 1; +} +``` + +`step(1.5, float(triVertexId))` будет 1, когда 1.5 меньше `triVertexId`. +Это верно для первых 2 вершин каждого треугольника и ложно +для последней. + +Мы можем получить вершины треугольника для круга так + +```glsl +int numSlices = 8; +int sliceId = gl_VertexID / 3; +int triVertexId = gl_VertexID % 3; +int edge = triVertexId + sliceId; +float angleU = float(edge) / float(numSlices); // 0.0 до 1.0 +float angle = angleU * PI * 2.0; +float radius = step(float(triVertexId), 1.5); +vec2 pos = vec2(cos(angle), sin(angle)) * radius; +``` + +Собрав все это вместе, давайте просто попробуем нарисовать 1 круг. + +```glsl +#version 300 es +uniform int numVerts; +uniform vec2 resolution; + +#define PI radians(180.0) + +void main() { + int numSlices = 8; + int sliceId = gl_VertexID / 3; + int triVertexId = gl_VertexID % 3; + int edge = triVertexId + sliceId; + float angleU = float(edge) / float(numSlices); // 0.0 до 1.0 + float angle = angleU * PI * 2.0; + float radius = step(float(triVertexId), 1.5); + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + + float aspect = resolution.y / resolution.x; + vec2 scale = vec2(aspect, 1); + + gl_Position = vec4(pos * scale, 0, 1); +} +``` + +Обратите внимание, мы вернули `resolution`, чтобы не получить эллипс. + +Для 8-срезового круга нам нужно 8 * 3 вершин + +```js +const numVerts = 8 * 3; +``` + +и нам нужно рисовать `TRIANGLES`, а не `POINTS` + +```js +const offset = 0; +gl.drawArrays(gl.TRIANGLES, offset, numVerts); +``` + +{{{example url="../webgl-no-data-triangles-circle.html"}}} + +А что, если бы мы хотели нарисовать несколько кругов? + +Все, что нам нужно сделать, это придумать `circleId`, который мы +можем использовать для выбора некоторой позиции для каждого круга, которая +одинакова для всех вершин в круге. + +```glsl +int numVertsPerCircle = numSlices * 3; +int circleId = gl_VertexID / numVertsPerCircle; +``` + +Например, давайте нарисуем круг из кругов. + +Сначала давайте превратим код выше в функцию, + +```glsl +vec2 computeCircleTriangleVertex(int vertexId) { + int numSlices = 8; + int sliceId = vertexId / 3; + int triVertexId = vertexId % 3; + int edge = triVertexId + sliceId; + float angleU = float(edge) / float(numSlices); // 0.0 до 1.0 + float angle = angleU * PI * 2.0; + float radius = step(float(triVertexId), 1.5); + return vec2(cos(angle), sin(angle)) * radius; +} +``` + +Теперь вот оригинальный код, который мы использовали для рисования +круга из точек в начале этой статьи. + +```glsl +float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 +float angle = u * PI * 2.0; // идет от 0 до 2PI +float radius = 0.8; + +vec2 pos = vec2(cos(angle), sin(angle)) * radius; + +float aspect = resolution.y / resolution.x; +vec2 scale = vec2(aspect, 1); + +gl_Position = vec4(pos * scale, 0, 1); +``` + +Нам просто нужно изменить его, чтобы использовать `circleId` вместо +`vertexId` и делить на количество кругов +вместо количества вершин. + +```glsl +void main() { + int circleId = gl_VertexID / numVertsPerCircle; + int numCircles = numVerts / numVertsPerCircle; + + float u = float(circleId) / float(numCircles); // идет от 0 до 1 + float angle = u * PI * 2.0; // идет от 0 до 2PI + float radius = 0.8; + + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + + vec2 triPos = computeCircleTriangleVertex(gl_VertexID) * 0.1; + + float aspect = resolution.y / resolution.x; + vec2 scale = vec2(aspect, 1); + + gl_Position = vec4((pos + triPos) * scale, 0, 1); +} +``` + +Затем нам просто нужно увеличить количество вершин + +```js +const numVerts = 8 * 3 * 20; +``` + +И теперь у нас есть круг из 20 кругов. + +{{{example url="../webgl-no-data-triangles-circles.html"}}} + +И, конечно, мы могли бы применить те же вещи, которые мы делали +выше, чтобы сделать дождь из кругов. Это, вероятно, не имеет +смысла, поэтому я не буду проходить через это, но это показывает +создание треугольников в вершинном шейдере без данных. + +Вышеуказанная техника могла бы использоваться для создания прямоугольников +или квадратов вместо этого, затем генерации UV координат, +передачи их в фрагментный шейдер и текстурирования +нашей сгенерированной геометрии. Это могло бы быть хорошо для +падающих снежинок или листьев, которые фактически переворачиваются в 3D, +применяя 3D техники, которые мы использовали в статьях +о [3D перспективе](webgl-3d-perspective.html). + +Я хочу подчеркнуть, что **эти техники** не являются обычными. +Создание простой системы частиц может быть полу-обычным или +эффект дождя выше, но создание чрезвычайно сложных вычислений +повредит производительности. В общем, если вы хотите производительности, +вы должны попросить ваш компьютер делать как можно меньше работы, +так что если есть куча вещей, которые вы можете предварительно вычислить во время инициализации +и передать в шейдер в той или иной форме, вы +должны сделать это. + +Например, вот экстремальный вершинный шейдер, +который вычисляет кучу кубов (предупреждение, есть звук). + + + +Как интеллектуальное любопытство головоломки "Если бы у меня не было данных, +кроме vertex id, мог бы я нарисовать что-то интересное?" это +довольно аккуратно. Фактически [весь этот сайт](https://www.vertexshaderart.com) о +головоломке, если у вас есть только vertex id, можете ли вы сделать что-то +интересное. Но для производительности было бы намного намного быстрее использовать +более традиционные техники передачи данных вершин куба +в буферы и чтения этих данных с атрибутами или другими техниками, +которые мы рассмотрим в других статьях. + +Есть некоторый баланс, который нужно найти. Для примера дождя выше, если вы хотите точно этот +эффект, то код выше довольно эффективен. Где-то между +двумя лежит граница, где одна техника более производительна, +чем другая. Обычно более традиционные техники намного более гибкие +также, но вам нужно решать на основе случая за случаем, когда использовать один +способ или другой. + +Цель этой статьи в основном познакомить с этими идеями +и подчеркнуть другие способы мышления о том, что WebGL +фактически делает. Снова ему все равно, что вы устанавливаете `gl_Position` +и выводите цвет в ваших шейдерах. Ему все равно, как вы это делаете. + +
+

Проблема с gl.POINTS

+

+Одна вещь, для которой техника вроде этой может быть полезной, это симуляция рисования +с gl.POINTS. +

+ +Есть 2 проблемы с gl.POINTS + +
    +
  1. У них максимальный размер

    Большинство людей, использующих gl.POINTS, используют маленькие размеры, +но если этот максимальный размер меньше, чем вам нужно, вам нужно будет выбрать другое решение. +
  2. +
  3. Как они обрезаются, когда за пределами экрана, непоследовательно

    +Проблема здесь в том, что представьте, что вы устанавливаете центр точки в 1 пиксель от левого края +canvas, но вы устанавливаете gl_PointSize в 32.0. +
    +Согласно спецификации OpenGL ES 3.0 +то, что должно произойти, это то, что поскольку 15 столбцов из этих 32x32 пикселей все еще на canvas, +они должны быть нарисованы. К сожалению, OpenGL (не ES) говорит прямо противоположное. +Если центр точки за пределами canvas, ничего не рисуется. Еще хуже, OpenGL до +недавнего времени был печально известен недостаточным тестированием, поэтому некоторые драйверы рисуют эти пиксели, +а некоторые нет 😭 +
  4. +
+

+Итак, если любая из этих проблем является проблемой для ваших потребностей, то как решение вам нужно рисовать свои собственные квады +с gl.TRIANGLES вместо использования gl.POINTS. + Если вы сделаете это, обе проблемы решены. +Проблема максимального размера исчезает, как и проблема непоследовательной обрезки. Есть различные +способы рисовать много квадов. Один из них использует техники вроде тех, что в этой статье.

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-fundamentals.md b/webgl/lessons/ru/webgl-fundamentals.md index 9c66748a0..789e23b3b 100644 --- a/webgl/lessons/ru/webgl-fundamentals.md +++ b/webgl/lessons/ru/webgl-fundamentals.md @@ -197,4 +197,419 @@ canvas. Вот простой пример WebGL, который показыв Фактически, большинство 3D движков генерируют GLSL шейдеры на лету, используя различные типы шаблонов, конкатенацию и т.д. Для примеров на этом сайте, однако, ни один из них не достаточно сложен, чтобы нуждаться в генерации GLSL во время выполнения. -> ПРИМЕЧАНИЕ: `#version 300 es` **ДОЛЖНА БЫТЬ САМОЙ ПЕРВОЙ СТРОКОЙ ВАШЕГО ШЕЙДЕРА**. Никаких комментариев или \ No newline at end of file +> ПРИМЕЧАНИЕ: `#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**, поскольку это намного +более гибко, чем любой другой метод. + +Чтобы количество пикселей в холсте соответствовало размеру, в котором он отображается, +[я использую вспомогательную функцию, о которой вы можете прочитать здесь](webgl-resizing-the-canvas.html). + +Почти во всех этих примерах размер холста составляет 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]` в холст. + +Вот живая версия + +{{{example url="../webgl-fundamentals.html" }}} + +В случае выше вы можете видеть, что наш вершинный шейдер ничего не делает, +кроме передачи наших данных позиции напрямую. Поскольку данные позиции уже +в 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`](/webgl/resources/webgl-utils.js), +который содержит функции для компиляции и связывания шейдеров. Нет причин загромождать примеры +этим [boilerplate](webgl-boilerplate.html) кодом. + +{{{example url="../webgl-2d-rectangle.html" }}} + +Снова вы можете заметить, что прямоугольник находится рядом с нижней частью этой области. WebGL считает положительный Y +вверх, а отрицательный Y вниз. В clip space левый нижний угол -1,-1. Мы не изменили никаких знаков, +поэтому с нашей текущей математикой 0, 0 становится левым нижним углом. +Чтобы получить более традиционный левый верхний угол, используемый для 2D графических API, +мы можем просто перевернуть координату y clip space. + + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); + +И теперь наш прямоугольник находится там, где мы ожидаем. + +{{{example url="../webgl-2d-rectangle-top-left.html" }}} + +Давайте сделаем код, который определяет прямоугольник, функцией, чтобы +мы могли вызывать её для прямоугольников разных размеров. Пока мы этим занимаемся, +мы сделаем цвет настраиваемым. + +Сначала мы делаем фрагментный шейдер принимающим 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); + } + +И вот прямоугольники. + +{{{example url="../webgl-2d-rectangles.html" }}} + +Я надеюсь, вы можете видеть, что WebGL на самом деле довольно простой API. +Хорошо, простой может быть неправильным словом. То, что он делает, простое. Он просто +выполняет 2 пользовательские функции, вершинный шейдер и фрагментный шейдер, и +рисует треугольники, линии или точки. +Хотя это может стать более сложным для 3D, эта сложность +добавляется вами, программистом, в виде более сложных шейдеров. +Сам API WebGL - это просто растеризатор и концептуально довольно прост. + +Мы рассмотрели небольшой пример, который показал, как предоставлять данные в атрибуте и 2 uniforms. +Обычно иметь несколько атрибутов и много uniforms. Ближе к началу этой статьи +мы также упомянули *varyings* и *текстуры*. Они появятся в последующих уроках. + +Прежде чем мы двинемся дальше, я хочу упомянуть, что для *большинства* приложений обновление +данных в буфере, как мы делали в `setRectangle`, не является обычным. Я использовал этот +пример, потому что думал, что его легче всего объяснить, поскольку он показывает координаты пикселей +как ввод и демонстрирует выполнение небольшого количества математики в GLSL. Это не неправильно, есть +множество случаев, где это правильная вещь для делать, но вы должны [продолжить читать, чтобы найти +более обычный способ позиционировать, ориентировать и масштабировать вещи в WebGL](webgl-2d-translation.html). + +Если вы на 100% новичок в WebGL и не имеете представления о том, что такое GLSL или шейдеры или что делает GPU, +тогда посмотрите [основы того, как WebGL действительно работает](webgl-how-it-works.html). +Вы также можете взглянуть на эту +[интерактивную диаграмму состояния](/webgl/lessons/resources/webgl-state-diagram.html) +для другого способа понимания того, как работает WebGL. + +Вы также должны, по крайней мере кратко прочитать о [boilerplate коде, используемом здесь](webgl-boilerplate.html), +который используется в большинстве примеров. Вы также должны хотя бы бегло просмотреть +[как рисовать несколько вещей](webgl-drawing-multiple-things.html), чтобы дать вам некоторое представление +о том, как структурированы более типичные WebGL приложения, потому что, к сожалению, почти все примеры +рисуют только одну вещь и поэтому не показывают эту структуру. + +Иначе отсюда вы можете пойти в 2 направлениях. Если вас интересует обработка изображений, +я покажу вам [как делать некоторую 2D обработку изображений](webgl-image-processing.html). +Если вас интересует изучение трансляции, +вращения и масштабирования, тогда [начните отсюда](webgl-2d-translation.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-gpgpu.md b/webgl/lessons/ru/webgl-gpgpu.md index 78abc6fd0..9bb0627a8 100644 --- a/webgl/lessons/ru/webgl-gpgpu.md +++ b/webgl/lessons/ru/webgl-gpgpu.md @@ -377,7 +377,7 @@ void main() { const program = webglUtils.createProgramFromSources(gl, [vs, fs]); const positionLoc = gl.getAttribLocation(program, 'position'); const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); -+const dstDimensionsLoc = gl.getUniformLocation(program, 'dstDimensions'); +const dstDimensionsLoc = gl.getUniformLocation(program, 'dstDimensions'); ``` и установить его @@ -385,15 +385,14 @@ const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); ```js gl.useProgram(program); gl.uniform1i(srcTexLoc, 0); // говорим шейдеру, что исходная текстура находится на texture unit 0 -+gl.uniform2f(dstDimensionsLoc, dstWidth, dstHeight); +gl.uniform2f(dstDimensionsLoc, dstWidth, dstHeight); ``` и нам нужно изменить размер назначения (canvas) ```js const dstWidth = 3; --const dstHeight = 2; -+const dstHeight = 1; +const dstHeight = 1; ``` и с этим у нас теперь есть результирующий массив, способный выполнять математику @@ -980,4 +979,1004 @@ for each point minDistanceSoFar = MAX_VALUE for each line segment compute distance from point to line segment -``` \ No newline at end of file + if distance is < minDistanceSoFar + minDistanceSoFar = distance + closestLine = line segment +``` + +Для 500 точек, каждая проверяющая 1000 линий, это 500,000 проверок. +Современные GPU имеют сотни или тысячи ядер, поэтому если мы могли бы сделать это на +GPU, мы могли бы потенциально работать в сотни или тысячи раз быстрее. + +На этот раз, хотя мы можем поместить точки в буфер, как мы делали для частиц, +мы не можем поместить отрезки линий в буфер. Буферы предоставляют свои данные через +атрибуты. Это означает, что мы не можем случайно обращаться к любому значению по требованию, вместо +этого значения присваиваются атрибуту вне контроля шейдера. + +Итак, нам нужно поместить позиции линий в текстуру, которая, как мы указали +выше, является другим словом для 2D массива, хотя мы все еще можем обращаться с этим 2D +массивом как с 1D массивом, если хотим. + +Вот вершинный шейдер, который находит ближайшую линию для одной точки. +Это точно алгоритм перебора, как выше + +```js + const closestLineVS = `#version 300 es + in vec3 point; + + uniform sampler2D linesTex; + uniform int numLineSegments; + + flat out int closestNdx; + + vec4 getAs1D(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); + } + + // из https://stackoverflow.com/a/6853926/128511 + // a - это точка, b,c - это отрезок линии + float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) { + vec3 ba = a - b; + vec3 bc = c - b; + float d = dot(ba, bc); + float len = length(bc); + float param = 0.0; + if (len != 0.0) { + param = clamp(d / (len * len), 0.0, 1.0); + } + vec3 r = b + bc * param; + return distance(a, r); + } + + void main() { + ivec2 linesTexDimensions = textureSize(linesTex, 0); + + // находим ближайший отрезок линии + float minDist = 10000000.0; + int minIndex = -1; + for (int i = 0; i < numLineSegments; ++i) { + vec3 lineStart = getAs1D(linesTex, linesTexDimensions, i * 2).xyz; + vec3 lineEnd = getAs1D(linesTex, linesTexDimensions, i * 2 + 1).xyz; + float dist = distanceFromPointToLine(point, lineStart, lineEnd); + if (dist < minDist) { + minDist = dist; + minIndex = i; + } + } + + closestNdx = minIndex; + } + `; +``` + +Я переименовал `getValueFrom2DTextureAs1DArray` в `getAs1D` просто чтобы сделать +некоторые строки короче и более читаемыми. +В противном случае это довольно прямолинейная реализация алгоритма перебора, +который мы написали выше. + +`point` - это текущая точка. `linesTex` содержит точки для +отрезка линии парами, первая точка, за которой следует вторая точка. + +Сначала давайте создадим некоторые тестовые данные. Вот 2 точки и 5 линий. Они +дополнены 0, 0, потому что каждая будет храниться в RGBA текстуре. + +```js +const points = [ + 100, 100, + 200, 100, +]; +const lines = [ + 25, 50, + 25, 150, + 90, 50, + 90, 150, + 125, 50, + 125, 150, + 185, 50, + 185, 150, + 225, 50, + 225, 150, +]; +const numPoints = points.length / 2; +const numLineSegments = lines.length / 2 / 2; +``` + +Если мы нанесем их на график, они будут выглядеть так + + + +Линии пронумерованы от 0 до 4 слева направо, +поэтому если наш код работает, первая точка (красная) +должна получить значение 1 как ближайшая линия, вторая точка +(зеленая), должна получить значение 3. + +Давайте поместим точки в буфер, а также создадим буфер для хранения вычисленного +ближайшего индекса для каждого + +```js +const closestNdxBuffer = makeBuffer(gl, points.length * 4, gl.STATIC_DRAW); +const pointsBuffer = makeBuffer(gl, new Float32Array(points), gl.DYNAMIC_DRAW); +``` + +и давайте создадим текстуру для хранения всех конечных точек линий. + +```js +function createDataTexture(gl, data, numComponents, internalFormat, format, type) { + const numElements = data.length / numComponents; + + // вычисляем размер, который будет содержать все наши данные + const width = Math.ceil(Math.sqrt(numElements)); + const height = Math.ceil(numElements / width); + + const bin = new Float32Array(width * height * numComponents); + bin.set(data); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip level + internalFormat, + width, + height, + 0, // border + format, + type, + bin, + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + return {tex, dimensions: [width, height]}; +} + +const {tex: linesTex, dimensions: linesTexDimensions} = + createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT); +``` + +В данном случае мы позволяем коду выбрать размеры текстуры +и позволяем ему дополнить текстуру. Например, если мы дадим ему массив +с 7 записями, он поместит это в текстуру 3x3. Он возвращает +и текстуру, и размеры, которые он выбрал. Почему мы позволяем ему выбрать +размер? Потому что текстуры имеют максимальный размер. + +В идеале мы хотели бы просто смотреть на наши данные как на 1-мерный массив +позиций, 1-мерный массив точек линий и т.д. Поэтому мы могли бы просто +объявить текстуру как Nx1. К сожалению, GPU имеют максимальный +размер, и это может быть всего 1024 или 2048. Если лимит +был 1024 и нам нужно было 1025 значений в нашем массиве, нам пришлось бы поместить данные +в текстуру, скажем, 512x2. Помещая данные в квадрат, мы не +достигнем лимита, пока не достигнем максимального размера текстуры в квадрате. +Для лимита размера 1024 это позволило бы массивы более 1 миллиона значений. + +Далее компилируем шейдер и находим локации + +```js +const closestLinePrg = createProgram( + gl, [closestLineVS, closestLineFS], ['closestNdx']); + +const closestLinePrgLocs = { + point: gl.getAttribLocation(closestLinePrg, 'point'), + linesTex: gl.getUniformLocation(closestLinePrg, 'linesTex'), + numLineSegments: gl.getUniformLocation(closestLinePrg, 'numLineSegments'), +}; +``` + +И создаем вершинный массив + +```js +const closestLineVA = makeVertexArray(gl, [ + [pointsBuffer, closestLinePrgLocs.point], +]); +``` + +И создаем transform feedback + +```js +const closestLineTF = makeTransformFeedback(gl, closestNdxBuffer); +``` + +Теперь мы можем запустить вычисление + +```js +gl.useProgram(closestLinePrg); +gl.bindVertexArray(closestLineVA); +gl.uniform1i(closestLinePrgLocs.linesTex, 0); +gl.uniform1f(closestLinePrgLocs.numLineSegments, numLineSegments); + +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, linesTex); + +gl.enable(gl.RASTERIZER_DISCARD); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, closestLineTF); +gl.beginTransformFeedback(gl.POINTS); +gl.drawArrays(gl.POINTS, 0, numPoints); +gl.endTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); +gl.disable(gl.RASTERIZER_DISCARD); +``` + +И читаем результаты + +```js +const results = new Int32Array(numPoints); +gl.bindBuffer(gl.ARRAY_BUFFER, closestNdxBuffer); +gl.getBufferSubData(gl.ARRAY_BUFFER, 0, results); +console.log('results:', results); +``` + +Результаты должны быть `[1, 3]`, что означает, что точка 0 ближе всего к линии 1, +а точка 1 ближе всего к линии 3. + +{{{example url="../webgl-gpgpu-closest-line-results-transformfeedback.html"}}} + +## Следующий пример: Динамический transform feedback + +В предыдущем примере мы использовали transform feedback для записи результатов +в буфер. Но что, если мы хотим использовать эти результаты в следующем кадре? + +Вот пример, где мы используем transform feedback для создания анимации. +Мы создаем частицы, которые движутся по кругу, и используем transform feedback +для обновления их позиций каждый кадр. + +```js +const vs = `#version 300 es +in vec4 position; +in vec4 velocity; +in float age; + +uniform float u_time; +uniform float u_deltaTime; + +out vec4 v_position; +out vec4 v_velocity; +out float v_age; + +void main() { + v_position = position; + v_velocity = velocity; + v_age = age; +} +`; + +const fs = `#version 300 es +precision highp float; + +in vec4 v_position; +in vec4 v_velocity; +in float v_age; + +uniform float u_time; +uniform float u_deltaTime; + +out vec4 outColor; + +void main() { + // обновляем позицию + vec4 newPosition = v_position + v_velocity * u_deltaTime; + + // обновляем скорость (добавляем небольшое ускорение) + vec4 newVelocity = v_velocity + vec4(0.0, -9.8, 0.0, 0.0) * u_deltaTime; + + // увеличиваем возраст + float newAge = v_age + u_deltaTime; + + // если частица слишком старая, сбрасываем её + if (newAge > 5.0) { + newPosition = vec4(0.0, 0.0, 0.0, 1.0); + newVelocity = vec4( + sin(u_time + gl_FragCoord.x * 0.01) * 100.0, + cos(u_time + gl_FragCoord.y * 0.01) * 100.0, + 0.0, 0.0 + ); + newAge = 0.0; + } + + outColor = vec4(newPosition.xyz, newAge); +} +`; + +const numParticles = 1000; +const positions = new Float32Array(numParticles * 4); +const velocities = new Float32Array(numParticles * 4); +const ages = new Float32Array(numParticles); + +// инициализируем частицы +for (let i = 0; i < numParticles; ++i) { + const angle = (i / numParticles) * Math.PI * 2; + const radius = 100 + Math.random() * 50; + + positions[i * 4 + 0] = Math.cos(angle) * radius; + positions[i * 4 + 1] = Math.sin(angle) * radius; + positions[i * 4 + 2] = 0; + positions[i * 4 + 3] = 1; + + velocities[i * 4 + 0] = Math.cos(angle) * 50; + velocities[i * 4 + 1] = Math.sin(angle) * 50; + velocities[i * 4 + 2] = 0; + velocities[i * 4 + 3] = 0; + + ages[i] = Math.random() * 5; +} + +const positionBuffer = makeBuffer(gl, positions, gl.DYNAMIC_DRAW); +const velocityBuffer = makeBuffer(gl, velocities, gl.DYNAMIC_DRAW); +const ageBuffer = makeBuffer(gl, ages, gl.DYNAMIC_DRAW); + +const updateProgram = createProgram(gl, [vs, fs], ['v_position', 'v_velocity', 'v_age']); + +const updateProgramLocs = { + position: gl.getAttribLocation(updateProgram, 'position'), + velocity: gl.getAttribLocation(updateProgram, 'velocity'), + age: gl.getAttribLocation(updateProgram, 'age'), + time: gl.getUniformLocation(updateProgram, 'u_time'), + deltaTime: gl.getUniformLocation(updateProgram, 'u_deltaTime'), +}; + +const updateVA = makeVertexArray(gl, [ + [positionBuffer, updateProgramLocs.position], + [velocityBuffer, updateProgramLocs.velocity], + [ageBuffer, updateProgramLocs.age], +]); + +const updateTF = makeTransformFeedback(gl, positionBuffer); + +let then = 0; +function render(time) { + time *= 0.001; + const deltaTime = time - then; + then = time; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.clear(gl.COLOR_BUFFER_BIT); + + // обновляем частицы + gl.useProgram(updateProgram); + gl.bindVertexArray(updateVA); + gl.uniform1f(updateProgramLocs.time, time); + gl.uniform1f(updateProgramLocs.deltaTime, deltaTime); + + gl.enable(gl.RASTERIZER_DISCARD); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, updateTF); + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, numParticles); + gl.endTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + gl.disable(gl.RASTERIZER_DISCARD); + + // рисуем частицы + gl.useProgram(drawProgram); + gl.bindVertexArray(drawVA); + gl.uniformMatrix4fv( + drawProgramLocs.matrix, + false, + m4.orthographic(-gl.canvas.width/2, gl.canvas.width/2, + -gl.canvas.height/2, gl.canvas.height/2, -1, 1)); + gl.drawArrays(gl.POINTS, 0, numParticles); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +## Следующий пример: Визуализация результатов + +В предыдущем примере мы вычислили, какая линия ближе всего к каждой точке, +но мы только вывели результаты в консоль. Давайте создадим визуализацию, +которая покажет точки, линии и соединит каждую точку с ближайшей к ней линией. + +Сначала нам нужны шейдеры для рисования линий и точек: + +```js +const drawLinesVS = `#version 300 es +in vec4 position; +void main() { + gl_Position = position; +} +`; + +const drawLinesFS = `#version 300 es +precision highp float; +out vec4 outColor; +void main() { + outColor = vec4(0.5, 0.5, 0.5, 1); // серый цвет для всех линий +} +`; + +const drawClosestLinesVS = `#version 300 es +in int closestNdx; + +uniform sampler2D linesTex; +uniform mat4 matrix; +uniform float numPoints; + +out vec4 v_color; + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + // получаем координаты линии из текстуры + ivec2 texelCoord = ivec2(closestNdx, 0); + vec4 lineData = texelFetch(linesTex, texelCoord, 0); + + // выбираем начальную или конечную точку линии + int linePointId = closestNdx * 2 + gl_VertexID % 2; + vec2 linePoint = mix(lineData.xy, lineData.zw, gl_VertexID % 2); + + gl_Position = matrix * vec4(linePoint, 0, 1); + + // вычисляем цвет на основе ID точки + float hue = float(gl_InstanceID) / numPoints; + v_color = vec4(hsv2rgb(vec3(hue, 1, 1)), 1); +} +`; + +const drawClosestLinesPointsFS = `#version 300 es +precision highp float; +in vec4 v_color; +out vec4 outColor; +void main() { + outColor = v_color; +} +`; + +const drawPointsVS = `#version 300 es +in vec2 point; + +uniform mat4 matrix; +uniform float numPoints; + +out vec4 v_color; + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + gl_Position = matrix * vec4(point, 0, 1); + gl_PointSize = 10.0; + + // вычисляем цвет на основе ID точки + float hue = float(gl_VertexID) / numPoints; + v_color = vec4(hsv2rgb(vec3(hue, 1, 1)), 1); +} +`; +``` + +Мы передаем `closestNdx` как атрибут. Это результаты, которые мы сгенерировали. +Используя это, мы можем найти конкретную линию. Нам нужно нарисовать 2 точки на линию, +поэтому мы будем использовать [инстансированное рисование](webgl-instanced-drawing.html) +для рисования 2 точек на `closestNdx`. Затем мы можем использовать `gl_VertexID % 2` +для выбора начальной или конечной точки. + +Наконец, мы вычисляем цвет, используя тот же метод, который мы использовали при рисовании точек, +чтобы они соответствовали своим точкам. + +Нам нужно скомпилировать все эти новые программы шейдеров и найти местоположения: + +```js +const closestLinePrg = createProgram( + gl, [closestLineVS, closestLineFS], ['closestNdx']); +const drawLinesPrg = createProgram( + gl, [drawLinesVS, drawLinesFS]); +const drawClosestLinesPrg = createProgram( + gl, [drawClosestLinesVS, drawClosestLinesPointsFS]); +const drawPointsPrg = createProgram( + gl, [drawPointsVS, drawClosestLinesPointsFS]); + +const closestLinePrgLocs = { + point: gl.getAttribLocation(closestLinePrg, 'point'), + linesTex: gl.getUniformLocation(closestLinePrg, 'linesTex'), + numLineSegments: gl.getUniformLocation(closestLinePrg, 'numLineSegments'), +}; +const drawLinesPrgLocs = { + linesTex: gl.getUniformLocation(drawLinesPrg, 'linesTex'), + matrix: gl.getUniformLocation(drawLinesPrg, 'matrix'), +}; +const drawClosestLinesPrgLocs = { + closestNdx: gl.getAttribLocation(drawClosestLinesPrg, 'closestNdx'), + linesTex: gl.getUniformLocation(drawClosestLinesPrg, 'linesTex'), + matrix: gl.getUniformLocation(drawClosestLinesPrg, 'matrix'), + numPoints: gl.getUniformLocation(drawClosestLinesPrg, 'numPoints'), +}; +const drawPointsPrgLocs = { + point: gl.getAttribLocation(drawPointsPrg, 'point'), + matrix: gl.getUniformLocation(drawPointsPrg, 'matrix'), + numPoints: gl.getUniformLocation(drawPointsPrg, 'numPoints'), +}; +``` + +Нам нужны массивы вершин для рисования точек и ближайших линий: + +```js +const closestLinesVA = makeVertexArray(gl, [ + [pointsBuffer, closestLinePrgLocs.point], +]); + +const drawClosestLinesVA = gl.createVertexArray(); +gl.bindVertexArray(drawClosestLinesVA); +gl.bindBuffer(gl.ARRAY_BUFFER, closestNdxBuffer); +gl.enableVertexAttribArray(drawClosestLinesPrgLocs.closestNdx); +gl.vertexAttribIPointer(drawClosestLinesPrgLocs.closestNdx, 1, gl.INT, 0, 0); +gl.vertexAttribDivisor(drawClosestLinesPrgLocs.closestNdx, 1); + +const drawPointsVA = makeVertexArray(gl, [ + [pointsBuffer, drawPointsPrgLocs.point], +]); +``` + +Итак, во время рендеринга мы вычисляем результаты, как мы делали раньше, но +мы не ищем результаты с помощью `getBufferSubData`. Вместо этого мы просто +передаем их в соответствующие шейдеры. + +Сначала рисуем все линии серым цветом: + +```js +// рисуем все линии серым цветом +gl.bindFramebuffer(gl.FRAMEBUFFER, null); +gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + +gl.bindVertexArray(null); +gl.useProgram(drawLinesPrg); + +// привязываем текстуру линий к текстуре unit 0 +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, linesTex); + +// Говорим шейдеру использовать текстуру на текстуре unit 0 +gl.uniform1i(drawLinesPrgLocs.linesTex, 0); +gl.uniformMatrix4fv(drawLinesPrgLocs.matrix, false, matrix); + +gl.drawArrays(gl.LINES, 0, numLineSegments * 2); +``` + +Затем рисуем все ближайшие линии: + +```js +gl.bindVertexArray(drawClosestLinesVA); +gl.useProgram(drawClosestLinesPrg); + +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, linesTex); + +gl.uniform1i(drawClosestLinesPrgLocs.linesTex, 0); +gl.uniform1f(drawClosestLinesPrgLocs.numPoints, numPoints); +gl.uniformMatrix4fv(drawClosestLinesPrgLocs.matrix, false, matrix); + +gl.drawArraysInstanced(gl.LINES, 0, 2, numPoints); +``` + +и наконец рисуем каждую точку: + +```js +gl.bindVertexArray(drawPointsVA); +gl.useProgram(drawPointsPrg); + +gl.uniform1f(drawPointsPrgLocs.numPoints, numPoints); +gl.uniformMatrix4fv(drawPointsPrgLocs.matrix, false, matrix); + +gl.drawArrays(gl.POINTS, 0, numPoints); +``` + +Прежде чем запустить, давайте сделаем еще одну вещь. Добавим больше точек и линий: + +```js +function createPoints(numPoints, ranges) { + const points = []; + for (let i = 0; i < numPoints; ++i) { + points.push(...ranges.map(range => r(...range))); + } + return points; +} + +const r = (min, max) => min + Math.random() * (max - min); + +const points = createPoints(8, [[0, gl.canvas.width], [0, gl.canvas.height]]); +const lines = createPoints(125 * 2, [[0, gl.canvas.width], [0, gl.canvas.height]]); +const numPoints = points.length / 2; +const numLineSegments = lines.length / 2 / 2; +``` + +и если мы запустим это: + +{{{example url="../webgl-gpgpu-closest-line-transformfeedback.html"}}} + +Вы можете увеличить количество точек и линий, +но в какой-то момент вы не сможете сказать, какие +точки соответствуют каким линиям, но с меньшим числом +вы можете хотя бы визуально проверить, что это работает. + +Просто для удовольствия, давайте объединим пример с частицами и этот +пример. Мы будем использовать техники, которые мы использовали для обновления +позиций частиц, чтобы обновить точки. Для +обновления конечных точек линий мы сделаем то, что мы делали в +начале, и запишем результаты в текстуру. + +Для этого мы копируем `updatePositionFS` вершинный шейдер +из примера с частицами. Для линий, поскольку их значения +хранятся в текстуре, нам нужно переместить их точки в +фрагментном шейдере: + +```js +const updateLinesVS = `#version 300 es +in vec4 position; +void main() { + gl_Position = position; +} +`; + +const updateLinesFS = `#version 300 es +precision highp float; + +uniform sampler2D linesTex; +uniform sampler2D velocityTex; +uniform vec2 canvasDimensions; +uniform float deltaTime; + +out vec4 outColor; + +vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); +} + +void main() { + // вычисляем координаты текселя из gl_FragCoord; + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + + // получаем данные линии + vec4 lineData = texelFetch(linesTex, texelCoord, 0); + + // получаем скорость для этой линии + vec2 velocity = texelFetch(velocityTex, texelCoord, 0).xy; + + // обновляем позиции + vec2 newStart = euclideanModulo(lineData.xy + velocity * deltaTime, canvasDimensions); + vec2 newEnd = euclideanModulo(lineData.zw + velocity * deltaTime, canvasDimensions); + + outColor = vec4(newStart, newEnd); +} +`; +``` + +Теперь нам нужны буферы для хранения скоростей линий и программа для их обновления: + +```js +const lineVelocities = new Float32Array(numLineSegments * 2); +for (let i = 0; i < numLineSegments; ++i) { + lineVelocities[i * 2 + 0] = (Math.random() - 0.5) * 100; + lineVelocities[i * 2 + 1] = (Math.random() - 0.5) * 100; +} + +const lineVelocityBuffer = makeBuffer(gl, lineVelocities, gl.DYNAMIC_DRAW); +const lineVelocityTex = makeDataTexture(gl, lineVelocities, numLineSegments, 1); + +const updateLinesPrg = createProgram(gl, [updateLinesVS, updateLinesFS]); + +const updateLinesPrgLocs = { + linesTex: gl.getUniformLocation(updateLinesPrg, 'linesTex'), + velocityTex: gl.getUniformLocation(updateLinesPrg, 'velocityTex'), + canvasDimensions: gl.getUniformLocation(updateLinesPrg, 'canvasDimensions'), + deltaTime: gl.getUniformLocation(updateLinesPrg, 'deltaTime'), +}; +``` + +Теперь в нашем цикле рендеринга мы обновляем линии, затем точки, затем рисуем все: + +```js +function render(time) { + time *= 0.001; + const deltaTime = time - then; + then = time; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.clear(gl.COLOR_BUFFER_BIT); + + // обновляем линии + gl.bindFramebuffer(gl.FRAMEBUFFER, linesFramebuffer); + gl.viewport(0, 0, numLineSegments, 1); + + gl.useProgram(updateLinesPrg); + gl.bindVertexArray(null); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, linesTex); + gl.uniform1i(updateLinesPrgLocs.linesTex, 0); + + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, lineVelocityTex); + gl.uniform1i(updateLinesPrgLocs.velocityTex, 1); + + gl.uniform2f(updateLinesPrgLocs.canvasDimensions, gl.canvas.width, gl.canvas.height); + gl.uniform1f(updateLinesPrgLocs.deltaTime, deltaTime); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + + // обновляем точки + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.useProgram(updateProgram); + gl.bindVertexArray(updateVA); + gl.uniform1f(updateProgramLocs.time, time); + gl.uniform1f(updateProgramLocs.deltaTime, deltaTime); + + gl.enable(gl.RASTERIZER_DISCARD); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, updateTF); + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, numPoints); + gl.endTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + gl.disable(gl.RASTERIZER_DISCARD); + + // вычисляем ближайшие линии + gl.bindFramebuffer(gl.FRAMEBUFFER, closestLineFramebuffer); + gl.viewport(0, 0, numPoints, 1); + + gl.useProgram(closestLinePrg); + gl.bindVertexArray(closestLinesVA); + gl.uniform1i(closestLinePrgLocs.linesTex, 0); + gl.uniform1f(closestLinePrgLocs.numLineSegments, numLineSegments); + + gl.enable(gl.RASTERIZER_DISCARD); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, closestLineTF); + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, numPoints); + gl.endTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + gl.disable(gl.RASTERIZER_DISCARD); + + // рисуем все + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // рисуем все линии серым цветом + gl.bindVertexArray(null); + gl.useProgram(drawLinesPrg); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, linesTex); + gl.uniform1i(drawLinesPrgLocs.linesTex, 0); + gl.uniformMatrix4fv(drawLinesPrgLocs.matrix, false, matrix); + gl.drawArrays(gl.LINES, 0, numLineSegments * 2); + + // рисуем ближайшие линии + gl.bindVertexArray(drawClosestLinesVA); + gl.useProgram(drawClosestLinesPrg); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, linesTex); + gl.uniform1i(drawClosestLinesPrgLocs.linesTex, 0); + gl.uniform1f(drawClosestLinesPrgLocs.numPoints, numPoints); + gl.uniformMatrix4fv(drawClosestLinesPrgLocs.matrix, false, matrix); + gl.drawArraysInstanced(gl.LINES, 0, 2, numPoints); + + // рисуем точки + gl.bindVertexArray(drawPointsVA); + gl.useProgram(drawPointsPrg); + gl.uniform1f(drawPointsPrgLocs.numPoints, numPoints); + gl.uniformMatrix4fv(drawPointsPrgLocs.matrix, false, matrix); + gl.drawArrays(gl.POINTS, 0, numPoints); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +{{{example url="../webgl-gpgpu-closest-line-dynamic-transformfeedback.html"}}} + +## Важные замечания + +* GPGPU в WebGL1 в основном ограничен использованием 2D массивов в качестве вывода (текстуры). + WebGL2 добавляет возможность просто обрабатывать 1D массив произвольного размера через + transform feedback. + + Если вам интересно, посмотрите [ту же статью для webgl1](https://webglfundamentals.org/webgl/lessons/webgl-gpgpu.html), чтобы увидеть, как все это было сделано, используя только возможность + вывода в текстуры. Конечно, с небольшим размышлением это должно быть очевидно. + + Версии WebGL2, использующие текстуры вместо transform feedback, также доступны, + поскольку использование `texelFetch` и наличие большего количества форматов текстур немного изменяет + их реализации. + + * [частицы](../webgl-gpgpu-particles.html) + * [результаты ближайших линий](../webgl-gpgpu-closest-line-results.html) + * [визуализация ближайших линий](../webgl-gpgpu-closest-line.html) + * [динамические ближайшие линии](../webgl-gpgpu-closest-line-dynamic.html) + +* Ошибка Firefox + + Firefox начиная с версии 84 имеет [ошибку](https://bugzilla.mozilla.org/show_bug.cgi?id=1677552) в том, + что он неправильно требует наличия по крайней мере одного активного атрибута, который использует делитель 0 при вызове + `drawArraysIndexed`. Это означает, что пример выше, где мы рисуем ближайшие линии, используя + `drawArraysIndexed`, не работает. + + Чтобы обойти это, мы можем создать буфер, который просто содержит `[0, 1]` в нем, и использовать его + на атрибуте для того, как мы использовали `gl_VertexID % 2`. Вместо этого мы будем использовать + + ```glsl + in int endPoint; // нужно для firefox + + ... + -int linePointId = closestNdx * 2 + gl_VertexID % 2; + +int linePointId = closestNdx * 2 + endPoint; + ... + ``` + + что [сделает это работающим в firefox](../webgl/webgl-gpgpu-closest-line-dynamic-transformfeedback-ff.html). + +* GPU не имеют той же точности, что и CPU. + + Проверьте ваши результаты и убедитесь, что они приемлемы. + +* Есть накладные расходы на GPGPU. + + В первых нескольких примерах выше мы вычислили некоторые + данные, используя WebGL, а затем прочитали результаты. Настройка буферов и текстур, + установка атрибутов и uniform переменных занимает время. Достаточно времени, чтобы для чего-либо + меньше определенного размера было бы лучше просто сделать это в JavaScript. + Фактические примеры умножения 6 чисел или сложения 3 пар чисел + слишком малы для того, чтобы GPGPU был полезен. Где находится эта граница + не определено. Экспериментируйте, но просто догадка, что если вы не делаете по крайней мере + 1000 или больше вещей, оставьте это в JavaScript. + +* `readPixels` и `getBufferSubData` медленные + + Чтение результатов из WebGL медленное, поэтому важно избегать этого + как можно больше. В качестве примера ни система частиц выше, ни + пример динамических ближайших линий никогда + не читают результаты обратно в JavaScript. Где можете, держите результаты + на GPU как можно дольше. Другими словами, вы могли бы сделать что-то + вроде + + * вычисляем что-то на GPU + * читаем результат + * подготавливаем результат для следующего шага + * загружаем подготовленный результат на GPU + * вычисляем что-то на GPU + * читаем результат + * подготавливаем результат для следующего шага + * загружаем подготовленный результат на GPU + * вычисляем что-то на GPU + * читаем результат + + тогда как через творческие решения было бы намного быстрее, если бы вы могли + + * вычисляем что-то на GPU + * подготавливаем результат для следующего шага, используя GPU + * вычисляем что-то на GPU + * подготавливаем результат для следующего шага, используя GPU + * вычисляем что-то на GPU + * читаем результат + + Наш пример динамических ближайших линий делал это. Результаты никогда не покидают + GPU. + + В качестве другого примера я однажды написал шейдер для вычисления гистограммы. Затем я прочитал + результаты обратно в JavaScript, вычислил минимальные и максимальные значения, + затем нарисовал изображение обратно на canvas, используя эти минимальные и максимальные значения + как uniform переменные для автоматического выравнивания изображения. + + Но оказалось, что вместо чтения гистограммы обратно в JavaScript + я мог вместо этого запустить шейдер на самой гистограмме, который генерировал + 2-пиксельную текстуру с минимальными и максимальными значениями в текстуре. + + Я мог затем передать эту 2-пиксельную текстуру в 3-й шейдер, который + мог читать для минимальных и максимальных значений. Нет необходимости читать их из + GPU для установки uniform переменных. + + Аналогично для отображения самой гистограммы я сначала читал данные гистограммы + из GPU, но позже я вместо этого написал шейдер, который мог + визуализировать данные гистограммы напрямую, убрав необходимость читать их + обратно в JavaScript. + + Делая это, весь процесс оставался на GPU и, вероятно, был намного + быстрее. + +* GPU могут делать много вещей параллельно, но большинство не могут многозадачно так же, как + CPU может. GPU обычно не могут делать "[вытесняющую многозадачность](https://www.google.com/search?q=preemptive+multitasking)". + Это означает, что если вы дадите им очень сложный шейдер, который, скажем, занимает 5 минут для + выполнения, они потенциально заморозят всю вашу машину на 5 минут. + Большинство хорошо сделанных ОС справляются с этим, заставляя CPU проверять, сколько времени прошло + с тех пор, как они дали последнюю команду GPU. Если прошло слишком много времени (5-6 секунд) + и GPU не ответил, то их единственный вариант - сбросить GPU. + + Это одна из причин, почему WebGL может *потерять контекст* и вы получите сообщение "Aw, rats!" + или подобное. + + Легко дать GPU слишком много работы, но в графике это не *так* + часто доводить до уровня 5-6 секунд. Обычно это больше похоже на уровень 0.1 + секунды, что все еще плохо, но обычно вы хотите, чтобы графика работала быстро + и поэтому программист, надеюсь, оптимизирует или найдет другую технику + для поддержания отзывчивости их приложения. + + GPGPU, с другой стороны, вы можете действительно захотеть дать GPU тяжелую задачу + для выполнения. Здесь нет простого решения. Мобильный телефон имеет гораздо менее мощный + GPU, чем топовый ПК. Помимо собственного тайминга, нет способа + точно знать, сколько работы вы можете дать GPU, прежде чем это "слишком медленно" + + У меня нет решения для предложения. Только предупреждение, что в зависимости от того, что вы + пытаетесь сделать, вы можете столкнуться с этой проблемой. + +* Мобильные устройства обычно не поддерживают рендеринг в текстуры с плавающей точкой + + Есть различные способы обойти эту проблему. Один из способов - вы можете + использовать функции GLSL `floatBitsToInt`, `floatBitsToUint`, `IntBitsToFloat`, + и `UintBitsToFloat`. + + В качестве примера, [версия на основе текстур примера с частицами](../webgl-gpgpu-particles.html) + должна записывать в текстуры с плавающей точкой. Мы могли бы исправить это так, чтобы это не требовало их, объявив + нашу текстуру как тип `RG32I` (32-битные целочисленные текстуры), но все еще + загружать float значения. + + В шейдере нам нужно будет читать текстуры как целые числа и декодировать их + в float, а затем кодировать результат обратно в целые числа. Например: + + ```glsl + #version 300 es + precision highp float; + + -uniform highp sampler2D positionTex; + -uniform highp sampler2D velocityTex; + +uniform highp isampler2D positionTex; + +uniform highp isampler2D velocityTex; + uniform vec2 canvasDimensions; + uniform float deltaTime; + + out ivec4 outColor; + + vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); + } + + void main() { + // будет одна скорость на позицию + // поэтому текстура скорости и текстура позиции + // имеют одинаковый размер. + + // кроме того, мы генерируем новые позиции + // поэтому мы знаем, что наше назначение того же размера + // что и наш источник + + // вычисляем координаты текстуры из gl_FragCoord; + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + + - vec2 position = texelFetch(positionTex, texelCoord, 0).xy; + - vec2 velocity = texelFetch(velocityTex, texelCoord, 0).xy; + + vec2 position = intBitsToFloat(texelFetch(positionTex, texelCoord, 0).xy); + + vec2 velocity = intBitsToFloat(texelFetch(velocityTex, texelCoord, 0).xy); + vec2 newPosition = euclideanModulo(position + velocity * deltaTime, canvasDimensions); + + - outColor = vec4(newPosition, 0, 1); + + outColor = ivec4(floatBitsToInt(newPosition), 0, 1); + } + ``` + + [Вот рабочий пример](../webgl-gpgpu-particles-no-floating-point-textures.html) + +Я надеюсь, что эти примеры помогли вам понять ключевую идею GPGPU в WebGL +- это просто тот факт, что WebGL читает из и записывает в массивы **данных**, +а не пикселей. + +Шейдеры работают аналогично функциям `map` в том, что функция, которая вызывается +для каждого значения, не может решить, где будет храниться ее значение. +Скорее это решается извне функции. В случае WebGL +это решается тем, как вы настраиваете то, что рисуете. Как только вы вызываете `gl.drawXXX` +шейдер будет вызван для каждого нужного значения с вопросом "какое значение я должен +сделать этим?" + +И это действительно все. + +--- + +Поскольку мы создали некоторые частицы через GPGPU, есть [это замечательное видео](https://www.youtube.com/watch?v=X-iSQQgOd1A), которое во второй половине +использует compute шейдеры для симуляции "слизи". + +Используя техники выше вот это переведено в WebGL2. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-how-it-works.md b/webgl/lessons/ru/webgl-how-it-works.md index aa9e3ed68..e8864453b 100644 --- a/webgl/lessons/ru/webgl-how-it-works.md +++ b/webgl/lessons/ru/webgl-how-it-works.md @@ -197,4 +197,197 @@ v_color, который мы объявили. // ищем, куда должны идти данные вершин. var positionLocation = gl.getAttribLocation(program, "a_position"); - + var colorLocation = gl.getAttribLocation(program, "a_color"); \ No newline at end of file + + var colorLocation = gl.getAttribLocation(program, "a_color"); + ... + + // Создаем буфер для цветов. + + var buffer = gl.createBuffer(); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + + + + // Устанавливаем цвета. + + setColors(gl); + + // настраиваем атрибуты + ... + + // говорим атрибуту цвета, как извлекать данные из текущего ARRAY_BUFFER + + gl.enableVertexAttribArray(colorLocation); + + var size = 4; + + var type = gl.FLOAT; + + var normalize = false; + + var stride = 0; + + var offset = 0; + + gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset); + + ... + + +// Заполняем буфер цветами для 2 треугольников + +// которые составляют прямоугольник. + +function setColors(gl) { + + // Выбираем 2 случайных цвета. + + var r1 = Math.random(); + + var b1 = Math.random(); + + var g1 = Math.random(); + + + + var r2 = Math.random(); + + var b2 = Math.random(); + + var g2 = Math.random(); + + + + gl.bufferData( + + gl.ARRAY_BUFFER, + + new Float32Array( + + [ r1, b1, g1, 1, + + r1, b1, g1, 1, + + r1, b1, g1, 1, + + r2, b2, g2, 1, + + r2, b2, g2, 1, + + r2, b2, g2, 1]), + + gl.STATIC_DRAW); + +} + +И вот результат. + +{{{example url="../webgl-2d-rectangle-with-2-colors.html" }}} + +Обратите внимание, что у нас есть 2 треугольника сплошного цвета. Тем не менее, мы передаем значения +в *varying*, поэтому они варьируются или интерполируются по +треугольнику. Просто мы использовали тот же цвет на каждой из 3 вершин +каждого треугольника. Если мы сделаем каждый цвет разным, мы увидим +интерполяцию. + + // Заполняем буфер цветами для 2 треугольников + // которые составляют прямоугольник. + function setColors(gl) { + // Делаем каждую вершину разным цветом. + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array( + * [ Math.random(), Math.random(), Math.random(), 1, + * Math.random(), Math.random(), Math.random(), 1, + * Math.random(), Math.random(), Math.random(), 1, + * Math.random(), Math.random(), Math.random(), 1, + * Math.random(), Math.random(), Math.random(), 1, + * Math.random(), Math.random(), Math.random(), 1]), + gl.STATIC_DRAW); + } + +И теперь мы видим интерполированный *varying*. + +{{{example url="../webgl-2d-rectangle-with-random-colors.html" }}} + +Не очень захватывающе, я полагаю, но это демонстрирует использование более чем одного +атрибута и передачу данных от вершинного шейдера к фрагментному шейдеру. Если +вы посмотрите на [примеры обработки изображений](webgl-image-processing.html), +вы увидите, что они также используют дополнительный атрибут для передачи координат текстуры. + +## Что делают эти команды буфера и атрибута? + +Буферы - это способ получения данных вершин и других данных на вершину на +GPU. `gl.createBuffer` создает буфер. +`gl.bindBuffer` устанавливает этот буфер как буфер для работы. +`gl.bufferData` копирует данные в текущий буфер. + +Как только данные находятся в буфере, нам нужно сказать WebGL, как извлекать данные из +него и предоставлять их атрибутам вершинного шейдера. + +Для этого сначала мы спрашиваем WebGL, какие локации он назначил +атрибутам. Например, в коде выше у нас есть + + // ищем, куда должны идти данные вершин. + var positionLocation = gl.getAttribLocation(program, "a_position"); + var colorLocation = gl.getAttribLocation(program, "a_color"); + +Как только мы знаем локацию атрибута, мы выдаем 2 команды. + + gl.enableVertexAttribArray(location); + +Эта команда говорит WebGL, что мы хотим предоставить данные из буфера. + + gl.vertexAttribPointer( + location, + numComponents, + typeOfData, + normalizeFlag, + strideToNextPieceOfData, + offsetIntoBuffer); + +И эта команда говорит WebGL получать данные из буфера, который был последним +привязан с gl.bindBuffer, сколько компонентов на вершину (1 - 4), какой +тип данных (`BYTE`, `FLOAT`, `INT`, `UNSIGNED_SHORT`, и т.д.), шаг +который означает, сколько байт пропустить, чтобы получить от одного куска данных к +следующему куску данных, и смещение для того, как далеко в буфере находятся наши данные. + +Количество компонентов всегда от 1 до 4. + +Если вы используете 1 буфер на тип данных, то и шаг, и смещение могут +всегда быть 0. 0 для шага означает "использовать шаг, который соответствует типу и +размеру". 0 для смещения означает начать с начала буфера. Установка +их в значения, отличные от 0, более сложна, и хотя это может иметь некоторые +преимущества с точки зрения производительности, это не стоит усложнения, если только +вы не пытаетесь довести WebGL до его абсолютных пределов. + +Я надеюсь, что это проясняет буферы и атрибуты. + +Вы можете взглянуть на эту +[интерактивную диаграмму состояния](/webgl/lessons/resources/webgl-state-diagram.html) +для другого способа понимания того, как работает WebGL. + +Далее давайте пройдемся по [шейдерам и GLSL](webgl-shaders-and-glsl.html). + +

Для чего нужен normalizeFlag в vertexAttribPointer?

+

+Флаг нормализации предназначен для всех не-плавающих типов. Если вы передаете +false, то значения будут интерпретироваться как тип, которым они являются. BYTE идет +от -128 до 127, UNSIGNED_BYTE идет от 0 до 255, SHORT идет от -32768 до 32767 и т.д... +

+

+Если вы устанавливаете флаг нормализации в true, то значения BYTE (-128 до 127) +представляют значения -1.0 до +1.0, UNSIGNED_BYTE (0 до 255) становятся 0.0 до +1.0. +Нормализованный SHORT также идет от -1.0 до +1.0, просто у него больше разрешения, чем у BYTE. +

+

+Самое распространенное использование нормализованных данных - для цветов. Большую часть времени цвета +идут только от 0.0 до 1.0. Использование полного float для каждого красного, зеленого, синего и альфа +использовало бы 16 байт на вершину на цвет. Если у вас сложная геометрия, это +может сложиться в много байт. Вместо этого вы могли бы конвертировать ваши цвета в UNSIGNED_BYTE, +где 0 представляет 0.0, а 255 представляет 1.0. Теперь вам понадобилось бы только 4 байта на цвет +на вершину, экономия 75%. +

+

Давайте изменим наш код, чтобы делать это. Когда мы говорим WebGL, как извлекать наши цвета, мы бы использовали

+
+  var size = 4;
+*  var type = gl.UNSIGNED_BYTE;
+*  var normalize = true;
+  var stride = 0;
+  var offset = 0;
+  gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);
+
+

И когда мы заполняем наш буфер цветами, мы бы использовали

+
+// Заполняем буфер цветами для 2 треугольников
+// которые составляют прямоугольник.
+function setColors(gl) {
+  // Выбираем 2 случайных цвета.
+  var r1 = Math.random() * 256; // 0 до 255.99999
+  var b1 = Math.random() * 256; // эти значения
+  var g1 = Math.random() * 256; // будут обрезаны
+  var r2 = Math.random() * 256; // когда сохранены в
+  var b2 = Math.random() * 256; // Uint8Array
+  var g2 = Math.random() * 256;
+
+  gl.bufferData(
+      gl.ARRAY_BUFFER,
+      new Uint8Array(   // Uint8Array
+        [ r1, b1, g1, 255,
+          r1, b1, g1, 255,
+          r1, b1, g1, 255,
+          r2, b2, g2, 255,
+          r2, b2, g2, 255,
+          r2, b2, g2, 255]),
+      gl.STATIC_DRAW);
+}
+
+

+Вот этот пример. +

+ +{{{example url="../webgl-2d-rectangle-with-2-byte-colors.html" }}} +
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-image-processing-continued.md b/webgl/lessons/ru/webgl-image-processing-continued.md index 344cacbdc..d5f7da233 100644 --- a/webgl/lessons/ru/webgl-image-processing-continued.md +++ b/webgl/lessons/ru/webgl-image-processing-continued.md @@ -195,4 +195,83 @@ TOC: Продвинутая обработка изображений var count = 6; gl.drawArrays(primitiveType, offset, count); } -``` \ No newline at end of file +``` + +Вот рабочая версия с немного более гибким UI. Отметьте эффекты, +чтобы включить их. Перетаскивайте эффекты, чтобы изменить порядок их применения. + +{{{example url="../webgl-2d-image-processing.html" }}} + +Некоторые вещи, которые я должен объяснить. + +Вызов `gl.bindFramebuffer` с `null` говорит WebGL, что вы хотите рендерить +на canvas вместо одного из ваших framebuffer'ов. + +Также framebuffer'ы могут работать или не работать в зависимости от того, какие привязки +вы на них помещаете. Есть список того, какие типы и комбинации привязок +должны всегда работать. Используемая здесь, одна текстура `RGBA`/`UNSIGNED_BYTE`, +назначенная точке привязки `COLOR_ATTACHMENT0`, должна всегда работать. +Более экзотические форматы текстур и/или комбинации привязок могут не работать. +В этом случае вы должны привязать framebuffer и затем вызвать +`gl.checkFramebufferStatus` и посмотреть, возвращает ли он `gl.FRAMEBUFFER_COMPLETE`. +Если да, то все в порядке. Если нет, вам нужно будет сказать пользователю использовать +что-то другое. К счастью, WebGL2 поддерживает многие форматы и комбинации. + +WebGL должен преобразовывать из [clip space](webgl-fundamentals.html) обратно в пиксели. +Он делает это на основе настроек `gl.viewport`. Поскольку framebuffer'ы, +в которые мы рендерим, имеют другой размер, чем canvas, нам нужно установить +viewport соответствующим образом в зависимости от того, рендерим ли мы в текстуру или canvas. + +Наконец, в [оригинальном примере](webgl-fundamentals.html) мы переворачивали координату Y +при рендеринге, потому что WebGL отображает canvas с 0,0 в левом нижнем углу +вместо более традиционного для 2D левого верхнего угла. Это не нужно +при рендеринге в framebuffer. Поскольку framebuffer никогда не отображается, +какая часть является верхом и низом, не имеет значения. Все, что имеет значение, +это то, что пиксель 0,0 в framebuffer соответствует 0,0 в наших вычислениях. +Чтобы справиться с этим, я сделал возможным установить, переворачивать или нет, добавив +еще один uniform вход в вызов шейдера `u_flipY`. + +``` +... ++uniform float u_flipY; +... + +void main() { + ... ++ gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1); + ... +} +``` + +И затем мы можем установить это при рендеринге с помощью + +``` + ... ++ var flipYLocation = gl.getUniformLocation(program, "u_flipY"); + + ... + ++ // не переворачиваем ++ gl.uniform1f(flipYLocation, 1); + + ... + ++ // переворачиваем ++ gl.uniform1f(flipYLocation, -1); +``` + +Я сохранил этот пример простым, используя одну GLSL программу, которая может достичь +множественных эффектов. Если бы вы хотели делать полноценную обработку изображений, вам, вероятно, +понадобилось бы много GLSL программ. Программа для настройки оттенка, насыщенности и яркости. +Другая для яркости и контрастности. Одна для инвертирования, другая для настройки +уровней и т.д. Вам нужно будет изменить код для переключения GLSL программ и обновления +параметров для этой конкретной программы. Я рассматривал написание этого примера, +но это упражнение лучше оставить читателю, потому что множественные GLSL программы, каждая +со своими потребностями в параметрах, вероятно, означает серьезный рефакторинг, чтобы все +не превратилось в большую путаницу спагетти-кода. + +Я надеюсь, что этот и предыдущие примеры сделали WebGL немного более +доступным, и я надеюсь, что начало с 2D помогает сделать WebGL немного легче для +понимания. Если я найду время, я попробую написать [еще несколько статей](webgl-2d-translation.html) +о том, как делать 3D, а также больше деталей о [том, что WebGL действительно делает под капотом](webgl-how-it-works.html). +Для следующего шага рассмотрите изучение [как использовать 2 или более текстур](webgl-2-textures.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-image-processing.md b/webgl/lessons/ru/webgl-image-processing.md index 5665f0090..a04a6387a 100644 --- a/webgl/lessons/ru/webgl-image-processing.md +++ b/webgl/lessons/ru/webgl-image-processing.md @@ -190,5 +190,140 @@ in vec2 v_texCoord; out vec4 outColor; void main() { -+ vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0)); -``` \ No newline at end of file + vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0)); + + // усредняем левый, средний и правый пиксели + outColor = ( + texture(u_image, v_texCoord) + + texture(u_image, v_texCoord + vec2( onePixel.x, 0.0)) + + texture(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0; +} +``` + +Сравните с неразмытым изображением выше. + +{{{example url="../webgl-2d-image-blend.html" }}} + +Теперь, когда мы знаем, как ссылаться на другие пиксели, давайте используем свёрточное ядро +для выполнения множества распространённых операций обработки изображений. В этом случае мы будем использовать ядро 3x3. +Свёрточное ядро — это просто матрица 3x3, где каждый элемент матрицы представляет +насколько умножить 8 пикселей вокруг пикселя, который мы рендерим. Затем мы +делим результат на вес ядра (сумма всех значений в ядре) +или 1.0, в зависимости от того, что больше. [Вот довольно хорошая статья об этом](https://docs.gimp.org/2.6/en/plug-in-convmatrix.html). +И [вот ещё одна статья, показывающая реальный код, если бы +вы писали это вручную на C++](https://www.codeproject.com/KB/graphics/ImageConvolution.aspx). + +В нашем случае мы будем делать эту работу в шейдере, так что вот новый фрагментный шейдер: + +``` +#version 300 es + +// фрагментные шейдеры не имеют точности по умолчанию, поэтому нужно +// выбрать одну. highp — хороший выбор по умолчанию. Это "высокая точность" +precision highp float; + +// наша текстура +uniform sampler2D u_image; + +// данные свёрточного ядра +uniform float u_kernel[9]; +uniform float u_kernelWeight; + +// координаты текстуры, переданные из вершинного шейдера +in vec2 v_texCoord; + +// объявляем выход для фрагментного шейдера +out vec4 outColor; + +void main() { + vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0)); + + vec4 colorSum = + texture(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] + + texture(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] + + texture(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] + + texture(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] + + texture(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] + + texture(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] + + texture(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] + + texture(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] + + texture(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ; + outColor = vec4((colorSum / u_kernelWeight).rgb, 1); +} +``` + +В JavaScript нам нужно предоставить свёрточное ядро и его вес: + + function computeKernelWeight(kernel) { + var weight = kernel.reduce(function(prev, curr) { + return prev + curr; + }); + return weight <= 0 ? 1 : weight; + } + + ... + var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]"); + var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight"); + ... + var edgeDetectKernel = [ + -1, -1, -1, + -1, 8, -1, + -1, -1, -1 + ]; + + // задаём ядро и его вес + gl.uniform1fv(kernelLocation, edgeDetectKernel); + gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel)); + ... + +И вуаля... Используйте выпадающий список для выбора разных ядер. + +{{{example url="../webgl-2d-image-3x3-convolution.html" }}} + +Я надеюсь, что эта статья убедила вас, что обработка изображений в WebGL довольно проста. Далее +я расскажу [как применить более одного эффекта к изображению](webgl-image-processing-continued.html). + +
+

Что такое текстурные юниты?

+Когда вы вызываете gl.draw??? ваш шейдер может ссылаться на текстуры. Текстуры привязаны +к текстурным юнитам. Хотя машина пользователя может поддерживать больше, все реализации WebGL2 +обязаны поддерживать как минимум 16 текстурных юнитов. К какому текстурному юниту ссылается каждый sampler uniform, +устанавливается путём поиска местоположения этого sampler uniform и затем установки +индекса текстурного юнита, на который вы хотите, чтобы он ссылался. + +Например: +
+var textureUnitIndex = 6; // используем текстурный юнит 6.
+var u_imageLoc = gl.getUniformLocation(
+    program, "u_image");
+gl.uniform1i(u_imageLoc, textureUnitIndex);
+
+ +Чтобы установить текстуры на разных юнитах, вы вызываете gl.activeTexture и затем привязываете текстуру, которую хотите на этом юните. Пример: + +
+// Привязываем someTexture к текстурному юниту 6.
+gl.activeTexture(gl.TEXTURE6);
+gl.bindTexture(gl.TEXTURE_2D, someTexture);
+
+ +Это тоже работает: + +
+var textureUnitIndex = 6; // используем текстурный юнит 6.
+// Привязываем someTexture к текстурному юниту 6.
+gl.activeTexture(gl.TEXTURE0 + textureUnitIndex);
+gl.bindTexture(gl.TEXTURE_2D, someTexture);
+
+
+ +
+

Что означают префиксы a_, u_, и v_ перед переменными в GLSL?

+

+Это просто соглашение об именовании. Они не обязательны, но для меня это делает легче увидеть с первого взгляда, +откуда приходят значения. a_ для атрибутов, которые являются данными, предоставленными буферами. u_ для uniform'ов, +которые являются входами в шейдеры, v_ для varying'ов, которые являются значениями, переданными из вершинного шейдера во +фрагментный шейдер и интерполированными (или изменёнными) между вершинами для каждого нарисованного пикселя. +Смотрите Как это работает для более подробной информации. +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-instanced-drawing.md b/webgl/lessons/ru/webgl-instanced-drawing.md index 9f4000e22..5ecefd98b 100644 --- a/webgl/lessons/ru/webgl-instanced-drawing.md +++ b/webgl/lessons/ru/webgl-instanced-drawing.md @@ -205,14 +205,53 @@ void main() { `; ``` ---- +Теперь нам нужно изменить шейдеры, чтобы использовать атрибуты вместо uniform'ов: -Теперь нам нужно реализовать инстансинг через атрибуты и буферы: +```js +const vertexShaderSource = `#version 300 es +in vec4 a_position; +in vec4 color; +in mat4 matrix; + +out vec4 v_color; + +void main() { + // Умножаем позицию на матрицу + gl_Position = matrix * a_position; + + // Передаём цвет вершины во фрагментный шейдер + v_color = color; +} +`; + +const fragmentShaderSource = `#version 300 es +precision highp float; + +in vec4 v_color; + +out vec4 outColor; + +void main() { + outColor = v_color; +} +`; +``` + +Теперь нам нужно получить локации атрибутов вместо uniform'ов: ```js -// настраиваем матрицы, по одной на экземпляр -const numInstances = 5; -// создаём типизированный массив с одним view на матрицу +const program = webglUtils.createProgramFromSources(gl, + [vertexShaderSource, fragmentShaderSource]); + +const positionLoc = gl.getAttribLocation(program, 'a_position'); +const colorLoc = gl.getAttribLocation(program, 'color'); +const matrixLoc = gl.getAttribLocation(program, 'matrix'); +``` + +Теперь нам нужно создать буферы для матриц и цветов. Для матриц мы создадим один большой буфер: + +```js +// Создаём буфер для всех матриц const matrixData = new Float32Array(numInstances * 16); const matrices = []; for (let i = 0; i < numInstances; ++i) { @@ -223,29 +262,64 @@ for (let i = 0; i < numInstances; ++i) { byteOffsetToMatrix, numFloatsForView)); } +``` +Таким образом, когда мы хотим ссылаться на данные всех матриц, +мы можем использовать `matrixData`, но когда мы хотим любую отдельную матрицу, +мы можем использовать `matrices[ndx]`. + +Нам также нужно создать буфер на GPU для этих данных. +Нам нужно только выделить буфер в этот момент, нам не нужно +предоставлять данные, поэтому 2-й параметр для `gl.bufferData` +- это размер, который просто выделяет буфер. + +```js const matrixBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); +// просто выделяем буфер gl.bufferData(gl.ARRAY_BUFFER, matrixData.byteLength, gl.DYNAMIC_DRAW); +``` + +Обратите внимание, что мы передали `gl.DYNAMIC_DRAW` как последний параметр. Это *подсказка* +для WebGL, что мы будем часто изменять эти данные. + +Теперь нам нужно настроить атрибуты для матриц. +Атрибут матрицы - это `mat4`. `mat4` фактически использует +4 последовательных слота атрибутов. +```js const bytesPerMatrix = 4 * 16; for (let i = 0; i < 4; ++i) { const loc = matrixLoc + i; gl.enableVertexAttribArray(loc); - // stride и offset - const offset = i * 16; + // обратите внимание на stride и offset + const offset = i * 16; // 4 float на строку, 4 байта на float gl.vertexAttribPointer( - loc, - 4, - gl.FLOAT, - false, - bytesPerMatrix, - offset, + loc, // location + 4, // размер (сколько значений брать из буфера за итерацию) + gl.FLOAT, // тип данных в буфере + false, // нормализовать + bytesPerMatrix, // stride, количество байт для перехода к следующему набору значений + offset, // смещение в буфере ); + // эта строка говорит, что этот атрибут изменяется только раз в 1 экземпляр gl.vertexAttribDivisor(loc, 1); } +``` + +Самая важная точка относительно инстансированного рисования - это +вызов `gl.vertexAttribDivisor`. Он устанавливает, что этот +атрибут переходит к следующему значению только раз в экземпляр. +Это означает, что атрибуты `matrix` будут использовать первую матрицу для +каждой вершины первого экземпляра, вторую матрицу для +второго экземпляра и так далее. -// настраиваем цвета, по одному на экземпляр +Далее нам нужны цвета также в буфере. Эти данные не будут +изменяться, по крайней мере в этом примере, поэтому мы просто загрузим +данные. + +```js +// настраиваем цвета, один на экземпляр const colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, @@ -257,42 +331,109 @@ gl.bufferData(gl.ARRAY_BUFFER, 0, 1, 1, 1, // циан ]), gl.STATIC_DRAW); +``` + +Нам также нужно настроить атрибут цвета: + +```js +// устанавливаем атрибут для цвета gl.enableVertexAttribArray(colorLoc); gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0); +// эта строка говорит, что этот атрибут изменяется только раз в 1 экземпляр gl.vertexAttribDivisor(colorLoc, 1); +``` -// В рендере: -function render(time) { - time *= 0.001; - webglUtils.resizeCanvasToDisplaySize(gl.canvas); - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - gl.useProgram(program); - gl.bindVertexArray(vao); +Во время отрисовки вместо цикла по каждому экземпляру, +установки uniform'ов матрицы и цвета, а затем вызова draw, +мы сначала вычислим матрицу для каждого экземпляра. - // обновляем все матрицы - matrices.forEach((mat, ndx) => { - m4.translation(-0.5 + ndx * 0.25, 0, 0, mat); - m4.zRotate(mat, time * (0.1 + 0.1 * ndx), mat); - }); - // загружаем все матрицы в буфер - gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, matrixData); - - // рисуем все экземпляры одной командой - gl.drawArraysInstanced( - gl.TRIANGLES, - 0, // offset - numVertices, // количество вершин на экземпляр - numInstances // количество экземпляров - ); +```js +// обновляем все матрицы +matrices.forEach((mat, ndx) => { + m4.translation(-0.5 + ndx * 0.25, 0, 0, mat); + m4.zRotate(mat, time * (0.1 + 0.1 * ndx), mat); +}); +``` - requestAnimationFrame(render); -} -requestAnimationFrame(render); +Поскольку наша библиотека матриц принимает необязательную матрицу назначения +и поскольку наши матрицы - это просто представления `Float32Array` в +большем `Float32Array`, когда мы закончили, все данные матриц +готовы для прямой загрузки на GPU. + +```js +// загружаем новые данные матриц +gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); +gl.bufferSubData(gl.ARRAY_BUFFER, 0, matrixData); ``` -Теперь мы вызываем только одну команду отрисовки, и WebGL сам перебирает экземпляры, используя данные из буферов для каждого экземпляра. +Наконец мы можем нарисовать все экземпляры одним вызовом draw. + +```js +gl.drawArraysInstanced( + gl.TRIANGLES, + 0, // offset + numVertices, // количество вершин на экземпляр + numInstances, // количество экземпляров +); +``` {{{example url="../webgl-instanced-drawing.html"}}} -Инстансинг — мощный способ ускорить отрисовку множества одинаковых объектов с разными параметрами. \ No newline at end of file +В примере выше у нас было 3 вызова WebGL на фигуру * 5 фигур, +что составляло 15 вызовов всего. Теперь у нас всего 2 вызова для всех 5 фигур, +один для загрузки матриц, другой для рисования. + +Я думаю, это должно быть очевидно, но, возможно, +это очевидно только мне, потому что я делал это слишком много. Код +выше не учитывает соотношение сторон canvas. +Он не использует [матрицу проекции](webgl-3d-orthographic.html) +или [матрицу вида](webgl-3d-camera.html). Он был предназначен только +для демонстрации инстансированного рисования. Если бы вы хотели проекцию и/или +матрицу вида, мы могли бы добавить вычисление в JavaScript. Это означало бы +больше работы для JavaScript. Более очевидный способ - добавить +один или два uniform'а в вершинный шейдер. + +```js +const vertexShaderSource = `#version 300 es +in vec4 a_position; +in vec4 color; +in mat4 matrix; +uniform mat4 projection; +uniform mat4 view; + +out vec4 v_color; + +void main() { + // Умножаем позицию на матрицу + gl_Position = projection * view * matrix * a_position; + + // Передаём цвет вершины во фрагментный шейдер + v_color = color; +} +`; +``` + +и затем найти их локации во время инициализации: + +```js +const positionLoc = gl.getAttribLocation(program, 'a_position'); +const colorLoc = gl.getAttribLocation(program, 'color'); +const matrixLoc = gl.getAttribLocation(program, 'matrix'); +const projectionLoc = gl.getUniformLocation(program, 'projection'); +const viewLoc = gl.getUniformLocation(program, 'view'); +``` + +и установить их соответствующим образом во время рендеринга. + +```js +gl.useProgram(program); + +// устанавливаем матрицы вида и проекции, поскольку +// они используются всеми экземплярами +const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; +gl.uniformMatrix4fv(projectionLoc, false, + m4.orthographic(-aspect, aspect, -1, 1, -1, 1)); +gl.uniformMatrix4fv(viewLoc, false, m4.zRotation(time * .1)); +``` + +{{{example url="../webgl-instanced-drawing-projection-view.html"}}} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-less-code-more-fun.md b/webgl/lessons/ru/webgl-less-code-more-fun.md index c3ba16672..090e7ee4e 100644 --- a/webgl/lessons/ru/webgl-less-code-more-fun.md +++ b/webgl/lessons/ru/webgl-less-code-more-fun.md @@ -196,4 +196,250 @@ var uniforms = { gl.useProgram(program); // Привязываем VAO, в котором уже все буферы и атрибуты -``` \ No newline at end of file +gl.bindVertexArray(vao); + +// Устанавливаем все uniform'ы и текстуры +twgl.setUniforms(uniformSetters, uniforms); + +gl.drawArrays(...); +``` + +Это кажется намного меньше, проще и с меньшим количеством кода. + +Вы даже можете использовать несколько JavaScript-объектов для uniform'ов, если это подходит. Например: + +``` +// При инициализации +var uniformSetters = twgl.createUniformSetters(gl, program); +var attribSetters = twgl.createAttributeSetters(gl, program); + +// Настраиваем все буферы и атрибуты +var attribs = { + a_position: { buffer: positionBuffer, numComponents: 3, }, + a_normal: { buffer: normalBuffer, numComponents: 3, }, + a_texcoord: { buffer: texcoordBuffer, numComponents: 2, }, +}; +var vao = twgl.createVAOAndSetAttributes(gl, attribSetters, attribs); + +// При инициализации или отрисовке +var uniformsThatAreTheSameForAllObjects = { + u_lightWorldPos: [100, 200, 300], + u_viewInverse: computeInverseViewMatrix(), + u_lightColor: [1, 1, 1, 1], +}; + +var uniformsThatAreComputedForEachObject = { + u_worldViewProjection: perspective(...), + u_world: computeWorldMatrix(), + u_worldInverseTranspose: computeWorldInverseTransposeMatrix(), +}; + +var objects = [ + { translation: [10, 50, 100], + materialUniforms: { + u_ambient: [0.1, 0.1, 0.1, 1], + u_diffuse: diffuseTexture, + u_specular: [1, 1, 1, 1], + u_shininess: 60, + u_specularFactor: 1, + }, + }, + { translation: [-120, 20, 44], + materialUniforms: { + u_ambient: [0.1, 0.2, 0.1, 1], + u_diffuse: someOtherDiffuseTexture, + u_specular: [1, 1, 0, 1], + u_shininess: 30, + u_specularFactor: 0.5, + }, + }, + { translation: [200, -23, -78], + materialUniforms: { + u_ambient: [0.2, 0.2, 0.1, 1], + u_diffuse: yetAnotherDiffuseTexture, + u_specular: [1, 0, 0, 1], + u_shininess: 45, + u_specularFactor: 0.7, + }, + }, +]; + +// При отрисовке +gl.useProgram(program); + +// Настраиваем части, общие для всех объектов +gl.bindVertexArray(vao); +twgl.setUniforms(uniformSetters, uniformsThatAreTheSameForAllObjects); + +objects.forEach(function(object) { + computeMatricesForObject(object, uniformsThatAreComputedForEachObject); + twgl.setUniforms(uniformSetters, uniformsThatAreComputedForEachObject); + twgl.setUniforms(uniformSetters, object.materialUniforms); + gl.drawArrays(...); +}); +``` + +Вот пример использования этих вспомогательных функций: + +{{{example url="../webgl-less-code-more-fun.html" }}} + +Давайте сделаем ещё один маленький шаг дальше. В коде выше мы настроили переменную `attribs` с буферами, которые создали. +Не показан код для настройки этих буферов. Например, если вы хотите создать позиции, нормали и координаты текстуры, +вам может понадобиться такой код: + + // один треугольник + var positions = [0, -10, 0, 10, 10, 0, -10, 10, 0]; + var texcoords = [0.5, 0, 1, 1, 0, 1]; + var normals = [0, 0, 1, 0, 0, 1, 0, 0, 1]; + + var positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + + var texcoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texcoords), gl.STATIC_DRAW); + + var normalBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); + +Похоже на паттерн, который мы тоже можем упростить: + + // один треугольник + var arrays = { + position: { numComponents: 3, data: [0, -10, 0, 10, 10, 0, -10, 10, 0], }, + texcoord: { numComponents: 2, data: [0.5, 0, 1, 1, 0, 1], }, + normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1], }, + }; + + var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); + var vao = twgl.createVAOFromBufferInfo(gl, setters, bufferInfo); + +Намного короче! + +Вот это: + +{{{example url="../webgl-less-code-more-fun-triangle.html" }}} + +Это будет работать даже если у нас есть индексы. `createVAOFromBufferInfo` +настроит все атрибуты и установит `ELEMENT_ARRAY_BUFFER` +с вашими `indices`, так что когда вы привяжете этот VAO, вы сможете вызвать +`gl.drawElements`. + + // индексированный квадрат + var arrays = { + position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], }, + texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1], }, + normal: { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], }, + indices: { numComponents: 3, data: [0, 1, 2, 1, 2, 3], }, + }; + + var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); + var vao = twgl.createVAOFromBufferInfo(gl, setters, bufferInfo); + +и во время рендеринга мы можем вызвать `gl.drawElements` вместо `gl.drawArrays`. + + ... + + // Рисуем геометрию + gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0); + +Вот это: + +{{{example url="../webgl-less-code-more-fun-quad.html" }}} + +Наконец, мы можем пойти, как я считаю, возможно, слишком далеко. Учитывая, что `position` почти всегда имеет 3 компонента (x, y, z), +`texcoords` почти всегда 2, индексы 3, а нормали 3, мы можем просто позволить системе угадать количество +компонентов. + + // индексированный квадрат + var arrays = { + position: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], + texcoord: [0, 0, 0, 1, 1, 0, 1, 1], + normal: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + indices: [0, 1, 2, 1, 2, 3], + }; + +И эта версия: + +{{{example url="../webgl-less-code-more-fun-quad-guess.html" }}} + +Я не уверен, что лично мне нравится этот стиль. Угадывание меня беспокоит, потому что оно может угадать неправильно. Например, +я могу решить добавить дополнительный набор координат текстуры в мой атрибут texcoord, и он +угадает 2 и будет неправ. Конечно, если он угадает неправильно, вы можете просто указать это, как в примере выше. +Я думаю, я беспокоюсь, что если код угадывания изменится, вещи людей могут сломаться. Это решать вам. Некоторым людям +нравится, когда вещи максимально простые, как они считают. + +Почему бы нам не посмотреть на атрибуты в шейдерной программе, чтобы выяснить количество компонентов? +Это потому, что часто предоставляют 3 компонента (x, y, z) из буфера, но используют `vec4` в +шейдере. Для атрибутов WebGL автоматически установит `w = 1`. Но это означает, что мы не можем легко +знать намерение пользователя, поскольку то, что они объявили в шейдере, может не соответствовать количеству +компонентов, которые они предоставляют. + +Ища больше паттернов, есть это: + + var program = twgl.createProgramFromSources(gl, [vs, fs]); + var uniformSetters = twgl.createUniformSetters(gl, program); + var attribSetters = twgl.createAttributeSetters(gl, program); + +Давайте упростим и это до просто: + + var programInfo = twgl.createProgramInfo(gl, ["vertexshader", "fragmentshader"]); + +Который возвращает что-то вроде: + + programInfo = { + program: WebGLProgram, // программа, которую мы только что скомпилировали + uniformSetters: ..., // setters, как возвращённые из createUniformSetters + attribSetters: ..., // setters, как возвращённые из createAttribSetters + } + +И это ещё одно небольшое упрощение. Это пригодится, когда мы начнём использовать +несколько программ, поскольку это автоматически держит setters с программой, с которой они связаны. + +{{{example url="../webgl-less-code-more-fun-quad-programinfo.html" }}} + +Ещё одно, иногда у нас есть данные без индексов, и мы должны вызывать +`gl.drawArrays`. В других случаях есть индексы, и мы должны вызывать `gl.drawElements`. +Учитывая данные, которые у нас есть, мы можем легко проверить что именно, посмотрев на `bufferInfo.indices`. +Если он существует, нам нужно вызвать `gl.drawElements`. Если нет, нам нужно вызвать `gl.drawArrays`. +Так что есть функция `twgl.drawBufferInfo`, которая делает это. Она используется так: + + twgl.drawBufferInfo(gl, bufferInfo); + +Если вы не передаёте 3-й параметр для типа примитива для рисования, он предполагает +`gl.TRIANGLES`. + +Вот пример, где у нас есть неиндексированный треугольник и индексированный квадрат. Поскольку +мы используем `twgl.drawBufferInfo`, код не должен изменяться, когда мы +переключаем данные. + +{{{example url="../webgl-less-code-more-fun-drawbufferinfo.html" }}} + +В любом случае, это стиль, в котором я пытаюсь писать свои собственные WebGL-программы. +Для уроков в этих туториалах, однако, я чувствовал, что должен использовать стандартные **многословные** +способы, чтобы люди не путались в том, что является WebGL, а что моим собственным стилем. В какой-то момент +показ всех шагов мешает сути, поэтому в будущем некоторые уроки будут +использовать этот стиль. + +Не стесняйтесь использовать этот стиль в своём собственном коде. Функции `twgl.createProgramInfo`, +`twgl.createVAOAndSetAttributes`, `twgl.createBufferInfoFromArrays` и `twgl.setUniforms` +и т.д. являются частью библиотеки, которую я написал на основе этих идей. [Она называется `TWGL`](https://twgljs.org). +Она рифмуется с wiggle и означает `Tiny WebGL`. + +Далее, [рисование множественных объектов](webgl-drawing-multiple-things.html). + +
+

Можем ли мы использовать setters напрямую?

+

+Для тех из вас, кто знаком с JavaScript, вы можете задаться вопросом, можете ли вы использовать setters +напрямую, как это: +

+
{{#escapehtml}}
+// При инициализации
+var uniformSetters = twgl.createUniformSetters(program);
+
+// При отрисовке
+uniformSetters.u_ambient([1, 0, 0, 1]); // установить цвет окружения в красный
+{{/escapehtml}}
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-load-obj-w-mtl.md b/webgl/lessons/ru/webgl-load-obj-w-mtl.md index f3f66f014..6caa93e97 100644 --- a/webgl/lessons/ru/webgl-load-obj-w-mtl.md +++ b/webgl/lessons/ru/webgl-load-obj-w-mtl.md @@ -265,236 +265,79 @@ void main () { `; ``` -Теперь у нас есть текстуры! - -{{{example url="../webgl-load-obj-w-mtl-w-textures.html"}}} - -Если посмотреть на .MTL-файл, можно увидеть `map_Ks` — это чёрно-белая текстура, которая определяет, насколько поверхность блестящая, или, иначе говоря, сколько specular-отражения используется. - -
- -Чтобы использовать её, нужно обновить шейдер, ведь мы уже загружаем все текстуры. - -```js -const fs = `#version 300 es -precision highp float; - -in vec3 v_normal; -in vec3 v_surfaceToView; -in vec2 v_texcoord; -in vec4 v_color; - -uniform vec3 diffuse; -uniform sampler2D diffuseMap; -uniform vec3 ambient; -uniform vec3 emissive; -uniform vec3 specular; -uniform sampler2D specularMap; -uniform float shininess; -uniform float opacity; -uniform vec3 u_lightDirection; -uniform vec3 u_ambientLight; - -out vec4 outColor; - -void main () { - vec3 normal = normalize(v_normal); - - vec3 surfaceToViewDirection = normalize(v_surfaceToView); - vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection); - - float fakeLight = dot(u_lightDirection, normal) * .5 + .5; - float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0); - vec4 specularMapColor = texture(specularMap, v_texcoord); - vec3 effectiveSpecular = specular * specularMapColor.rgb; - - vec4 diffuseMapColor = texture(diffuseMap, v_texcoord); - vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb; - float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a; - - outColor = vec4( - emissive + - ambient * u_ambientLight + - effectiveDiffuse * fakeLight + - effectiveSpecular * pow(specularLight, shininess), - effectiveOpacity); -``` - -Также стоит добавить значение по умолчанию для материалов без карты specular: - -```js -const defaultMaterial = { - diffuse: [1, 1, 1], - diffuseMap: textures.defaultWhite, - ambient: [0, 0, 0], - specular: [1, 1, 1], - specularMap: textures.defaultWhite, - shininess: 400, - opacity: 1, -}; -``` - -В .MTL-файле значения specular могут быть не очень наглядными, поэтому для наглядности можно «взломать» параметры specular: - -```js -// хак: делаем specular заметнее -Object.values(materials).forEach(m => { - m.shininess = 25; - m.specular = [3, 2, 1]; -}); -``` - -Теперь видно, что только окна и лопасти отражают свет. - -{{{example url="../webgl-load-obj-w-mtl-w-specular-map.html"}}} - -Меня удивило, что лопасти отражают свет. Если посмотреть на .MTL-файл, там shininess `Ns` = 0.0, что означает очень сильные specular-блики. Но illum = 1 для обоих материалов. По документации illum 1 означает: - -``` -color = KaIa + Kd { SUM j=1..ls, (N * Lj)Ij } -``` - -То есть: - -``` -color = ambientColor * lightAmbient + diffuseColor * sumOfLightCalculations -``` - -Как видно, specular тут не участвует, но в файле всё равно есть specular map! ¯\_(ツ)_/¯ Для specular-бликов нужен illum 2 или выше. Это типичная ситуация с .OBJ/.MTL: часто приходится вручную дорабатывать материалы. Как исправлять — решать вам: можно править .MTL, можно добавить код. Мы выбрали второй путь. - -Последняя карта в этом .MTL — `map_Bump` (bump map). На самом деле файл — это normal map. - -
- -В .MTL нет опции явно указать normal map или что bump map — это normal map. Можно использовать эвристику: если в имени файла есть 'nor', или просто считать, что все `map_Bump` — это normal map (по крайней мере в 2020+). Так и поступим. - -Для генерации тангенсов используем код из [статьи про normal mapping](webgl-3d-lighting-normal-mapping.html): - -```js -const parts = obj.geometries.map(({material, data}) => { - ... - - // генерируем тангенсы, если есть данные - if (data.texcoord && data.normal) { - data.tangent = generateTangents(data.position, data.texcoord); - } else { - // Нет тангенсов - data.tangent = { value: [1, 0, 0] }; - } - - // создаём буфер для каждого массива - const bufferInfo = twgl.createBufferInfoFromArrays(gl, data); - const vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, bufferInfo); - return { - material: { - ...defaultMaterial, - ...materials[material], - }, - bufferInfo, - vao, - }; -}); -``` - -Также добавим normal map по умолчанию для материалов, у которых его нет: - -```js -const textures = { - defaultWhite: twgl.createTexture(gl, {src: [255, 255, 255, 255]}), - defaultNormal: twgl.createTexture(gl, {src: [127, 127, 255, 0]}), -}; +И теперь мы получаем normal maps. Примечание: я приблизил камеру, чтобы их было легче увидеть. + +{{{example url="../webgl-load-obj-w-mtl-w-normal-maps.html"}}} + +Уверен, что в .MTL-файле есть гораздо больше возможностей, которые мы могли бы поддержать. +Например, ключевое слово `refl` указывает карты отражения, что является другим словом +для [environment map](webgl-environment-maps.html). Также показано, что различные +ключевые слова `map_` принимают множество опциональных аргументов. Несколько из них: + +* `-clamp on | off` указывает, повторяется ли текстура +* `-mm base gain` указывает смещение и множитель для значений текстуры +* `-o u v w` указывает смещение для координат текстуры. Вы бы применили их, используя матрицу текстуры, аналогично тому, что мы делали в [статье про drawImage](webgl-2d-drawimage.html) +* `-s u v w` указывает масштаб для координат текстуры. Как и выше, вы бы поместили их в матрицу текстуры + +Я не знаю, сколько .MTL-файлов используют эти настройки. + +Более важный момент заключается в том, что добавление поддержки каждой функции делает +шейдеры больше и сложнее. Выше у нас есть форма *uber shader*, +шейдер, который пытается обработать все случаи. Чтобы заставить его работать, мы передали различные +значения по умолчанию. Например, мы установили `diffuseMap` как белую текстуру, чтобы если мы +загружаем что-то без текстур, это всё равно отображалось. Diffuse цвет будет +умножен на белый, что равно 1.0, поэтому мы просто получим diffuse цвет. +Аналогично мы передали белый цвет вершины по умолчанию на случай, если нет +цветов вершин. + +Это распространённый способ заставить вещи работать, и если это работает достаточно быстро для ваших +потребностей, то нет причин это менять. Но более распространено генерировать +шейдеры, которые включают/выключают эти функции. Если нет цветов вершин, то +генерируйте шейдер, как в манипуляции со строками шейдеров, чтобы у них не было атрибута +`a_color` и всего связанного кода. Аналогично, если у материала нет diffuse map, то +генерируйте шейдер, у которого нет `uniform sampler2D diffuseMap` и удалите весь связанный код. +Если у него нет никаких карт, то нам не нужны координаты текстуры, поэтому мы их тоже оставим. + +Когда вы сложите все комбинации, может быть тысячи вариаций шейдеров. +Только с тем, что у нас есть выше, есть: + +* diffuseMap да/нет +* specularMap да/нет +* normalMap да/нет +* цвета вершин да/нет +* ambientMap да/нет (мы не поддерживали это, но .MTL файл поддерживает) +* reflectionMap да/нет (мы не поддерживали это, но .MTL файл поддерживает) + +Только эти представляют 64 комбинации. Если мы добавим, скажем, от 1 до 4 источников света, и эти +источники света могут быть spot, или point, или directional, мы получим 8192 возможных +комбинации функций шейдера. + +Управление всем этим — это много работы. Это одна из причин, почему многие люди +выбирают 3D движок, такой как [three.js](https://threejs.org), вместо того, чтобы делать это +всё самим. Но, по крайней мере, надеюсь, эта статья даёт некоторое представление о +типах вещей, связанных с отображением произвольного 3D контента. + +
+

Избегайте условных операторов в шейдерах где возможно

+

Традиционный совет — избегать условных операторов в шейдерах. В качестве примера +мы могли бы сделать что-то вроде этого

+
{{#escapehtml}}
+uniform bool hasDiffuseMap;
+uniform vec4 diffuse;
+uniform sampler2D diffuseMap
 
 ...
-
-const defaultMaterial = {
-  diffuse: [1, 1, 1],
-  diffuseMap: textures.defaultWhite,
-  normalMap: textures.defaultNormal,
-  ambient: [0, 0, 0],
-  specular: [1, 1, 1],
-  specularMap: textures.defaultWhite,
-  shininess: 400,
-  opacity: 1,
-};
+  vec4 effectiveDiffuse = diffuse;
+  if (hasDiffuseMap) {
+    effectiveDiffuse *= texture2D(diffuseMap, texcoord);
+  }
 ...
-```
-
-И, наконец, вносим изменения в шейдеры, как в [статье про normal mapping](webgl-3d-lighting-normal-mapping.html):
-
-```js
-const vs = `#version 300 es
-in vec4 a_position;
-in vec3 a_normal;
-in vec3 a_tangent;
-in vec2 a_texcoord;
-in vec4 a_color;
-
-uniform mat4 u_projection;
-uniform mat4 u_view;
-uniform mat4 u_world;
-uniform vec3 u_viewWorldPosition;
-
-out vec3 v_normal;
-out vec3 v_tangent;
-out vec3 v_surfaceToView;
-out vec2 v_texcoord;
-out vec4 v_color;
-
-void main() {
-  vec4 worldPosition = u_world * a_position;
-  gl_Position = u_projection * u_view * worldPosition;
-  v_surfaceToView = u_viewWorldPosition - worldPosition.xyz;
-
-  mat3 normalMat = mat3(u_world);
-  v_normal = normalize(normalMat * a_normal);
-  v_tangent = normalize(normalMat * a_tangent);
-
-  v_texcoord = a_texcoord;
-  v_color = a_color;
-}
-`;
-
-const fs = `#version 300 es
-precision highp float;
-
-in vec3 v_normal;
-in vec3 v_tangent;
-in vec3 v_surfaceToView;
-in vec2 v_texcoord;
-in vec4 v_color;
-
-uniform vec3 diffuse;
-uniform sampler2D diffuseMap;
-uniform vec3 ambient;
-uniform vec3 emissive;
-uniform vec3 specular;
-uniform sampler2D specularMap;
-uniform float shininess;
-uniform sampler2D normalMap;
-uniform float opacity;
-uniform vec3 u_lightDirection;
-uniform vec3 u_ambientLight;
-
-out vec4 outColor;
-
-void main () {
-  vec3 normal = normalize(v_normal);
-  vec3 tangent = normalize(v_tangent);
-  vec3 bitangent = normalize(cross(normal, tangent));
-
-  mat3 tbn = mat3(tangent, bitangent, normal);
-  normal = texture(normalMap, v_texcoord).rgb * 2. - 1.;
-  normal = normalize(tbn * normal);
-
-  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
-  vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection);
-
-  float fakeLight = dot(u_lightDirection, normal) * .5 + .5;
-  float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0);
-  vec4 specularMapColor = texture(specularMap, v_texcoord);
-  vec3 effectiveSpecular = specular * specularMapColor.rgb;
-
-  vec4 diffuseMapColor = texture(diffuseMap, v_texcoord);
-  vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb;
-  float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a;
-``` 
\ No newline at end of file
+{{/escapehtml}}
+

Условные операторы, такие как этот, обычно не рекомендуются, потому что в зависимости от +GPU/драйвера они часто не очень производительны.

+

Либо делайте, как мы сделали выше, и попытайтесь сделать код без условных операторов. Мы использовали +один 1x1 белый пиксель текстуры, когда нет текстуры, чтобы наша математика работала +без условного оператора.

+

Или используйте разные шейдеры. Один, у которого нет функции, и один, у которого есть, +и выбирайте правильный для каждой ситуации.

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-load-obj.md b/webgl/lessons/ru/webgl-load-obj.md index 54947608c..4ecaec231 100644 --- a/webgl/lessons/ru/webgl-load-obj.md +++ b/webgl/lessons/ru/webgl-load-obj.md @@ -179,77 +179,77 @@ f 1//1 2//2 3//3 # индексы для позиций и нормалей ```js function parseOBJ(text) { -+ // потому что индексы основаны на 1, давайте просто заполним 0-е данные -+ const objPositions = [[0, 0, 0]]; -+ const objTexcoords = [[0, 0]]; -+ const objNormals = [[0, 0, 0]]; -+ -+ // тот же порядок, что и индексы `f` -+ const objVertexData = [ -+ objPositions, -+ objTexcoords, -+ objNormals, -+ ]; -+ -+ // тот же порядок, что и индексы `f` -+ let webglVertexData = [ -+ [], // позиции -+ [], // текстурные координаты -+ [], // нормали -+ ]; -+ -+ function addVertex(vert) { -+ const ptn = vert.split('/'); -+ ptn.forEach((objIndexStr, i) => { -+ if (!objIndexStr) { -+ return; -+ } -+ const objIndex = parseInt(objIndexStr); -+ const index = objIndex + (objIndex >= 0 ? 0 : objVertexData[i].length); -+ webglVertexData[i].push(...objVertexData[i][index]); -+ }); -+ } -+ -+ const keywords = { -+ v(parts) { -+ objPositions.push(parts.map(parseFloat)); -+ }, -+ vn(parts) { -+ objNormals.push(parts.map(parseFloat)); -+ }, -+ vt(parts) { -+ objTexcoords.push(parts.map(parseFloat)); -+ }, -+ f(parts) { -+ const numTriangles = parts.length - 2; -+ for (let tri = 0; tri < numTriangles; ++tri) { -+ addVertex(parts[0]); -+ addVertex(parts[tri + 1]); -+ addVertex(parts[tri + 2]); -+ } -+ }, -+ }; -+ -+ const keywordRE = /(\w*)(?: )*(.*)/; -+ const lines = text.split('\n'); -+ for (let lineNo = 0; lineNo < lines.length; ++lineNo) { -+ const line = lines[lineNo].trim(); -+ if (line === '' || line.startsWith('#')) { -+ continue; -+ } -+ const m = keywordRE.exec(line); -+ if (!m) { -+ continue; -+ } -+ const [, keyword, unparsedArgs] = m; -+ const parts = line.split(/\s+/).slice(1); -+ const handler = keywords[keyword]; -+ if (!handler) { -+ console.warn('unhandled keyword:', keyword, 'at line', lineNo + 1); -+ continue; -+ } -+ handler(parts, unparsedArgs); -+ } + // потому что индексы основаны на 1, давайте просто заполним 0-е данные + const objPositions = [[0, 0, 0]]; + const objTexcoords = [[0, 0]]; + const objNormals = [[0, 0, 0]]; + + // тот же порядок, что и индексы `f` + const objVertexData = [ + objPositions, + objTexcoords, + objNormals, + ]; + + // тот же порядок, что и индексы `f` + let webglVertexData = [ + [], // позиции + [], // текстурные координаты + [], // нормали + ]; + + function addVertex(vert) { + const ptn = vert.split('/'); + ptn.forEach((objIndexStr, i) => { + if (!objIndexStr) { + return; + } + const objIndex = parseInt(objIndexStr); + const index = objIndex + (objIndex >= 0 ? 0 : objVertexData[i].length); + webglVertexData[i].push(...objVertexData[i][index]); + }); + } + + const keywords = { + v(parts) { + objPositions.push(parts.map(parseFloat)); + }, + vn(parts) { + objNormals.push(parts.map(parseFloat)); + }, + vt(parts) { + objTexcoords.push(parts.map(parseFloat)); + }, + f(parts) { + const numTriangles = parts.length - 2; + for (let tri = 0; tri < numTriangles; ++tri) { + addVertex(parts[0]); + addVertex(parts[tri + 1]); + addVertex(parts[tri + 2]); + } + }, + }; + + const keywordRE = /(\w*)(?: )*(.*)/; + const lines = text.split('\n'); + for (let lineNo = 0; lineNo < lines.length; ++lineNo) { + const line = lines[lineNo].trim(); + if (line === '' || line.startsWith('#')) { + continue; + } + const m = keywordRE.exec(line); + if (!m) { + continue; + } + const [, keyword, unparsedArgs] = m; + const parts = line.split(/\s+/).slice(1); + const handler = keywords[keyword]; + if (!handler) { + console.warn('unhandled keyword:', keyword, 'at line', lineNo + 1); + continue; + } + handler(parts, unparsedArgs); + } return { position: webglVertexData[0], @@ -569,4 +569,256 @@ function addVertex(vert) { Вы можете увидеть пример загрузки .GLTF файла в [статье о скиннинге](webgl-skinning.html). Если у вас есть .OBJ файлы, которые вы хотите использовать, лучшая практика - конвертировать их - в какой-то другой формат сначала, офлайн, а затем использовать лучший формат на вашей странице. \ No newline at end of file + в какой-то другой формат сначала, офлайн, а затем использовать лучший формат на вашей странице. + +## Загрузка .OBJ с цветами вершин + +Некоторые .OBJ файлы содержат цвета вершин. Это данные, которые хранятся в каждой вершине, +а не в материале. Давайте добавим поддержку для этого. + +Сначала нужно обновить парсер, чтобы он обрабатывал ключевое слово `vc` (vertex color): + +```js +function parseOBJ(text) { + const objPositions = []; + const objTexcoords = []; + const objNormals = []; + const objColors = []; + const objVertexData = [ + objPositions, + objTexcoords, + objNormals, + objColors, + ]; + + // индексы для webgl используют 0 как базу + const webglIndices = []; + let geometry; + let material = 'default'; + let object = 'default'; + + const noop = () => {}; + + const keywords = { + v(parts) { + // если есть 4 значения, то это позиция + цвет + if (parts.length === 6) { + objPositions.push(parts[0], parts[1], parts[2]); + objColors.push(parts[3], parts[4], parts[5]); + } else { + objPositions.push(parts[0], parts[1], parts[2]); + } + }, + vn(parts) { + objNormals.push(parts[0], parts[1], parts[2]); + }, + vt(parts) { + objTexcoords.push(parts[0], parts[1]); + }, + f(parts) { + setGeometry(); + addFace(parts); + }, + s: noop, // smoothing group + mtllib(parts, unparsedArgs) { + // материал библиотека + materialLib = unparsedArgs; + }, + usemtl(parts, unparsedArgs) { + material = unparsedArgs; + setGeometry(); + }, + g(parts, unparsedArgs) { + object = unparsedArgs; + setGeometry(); + }, + o(parts, unparsedArgs) { + object = unparsedArgs; + setGeometry(); + }, + }; + + function setGeometry() { + if (geometry) { + geometry = undefined; + } + } + + function addFace(parts) { + const numTriangles = parts.length - 2; + for (let tri = 0; tri < numTriangles; ++tri) { + addVertex(parts[0]); + addVertex(parts[tri + 1]); + addVertex(parts[tri + 2]); + } + } + + const keywordRE = /(\w*)(?: )*(.*)/; + const lines = text.split('\n'); + for (let lineNo = 0; lineNo < lines.length; ++lineNo) { + const line = lines[lineNo].trim(); + if (line === '' || line.startsWith('#')) { + continue; + } + const m = keywordRE.exec(line); + if (!m) { + continue; + } + const [, keyword, unparsedArgs] = m; + const parts = line.split(/\s+/).slice(1); + const handler = keywords[keyword]; + if (!handler) { + console.warn('unhandled keyword:', keyword); + continue; + } + handler(parts, unparsedArgs); + } + + for (const geometry of Object.values(geometries)) { + geometry.data = {}; + if (geometry.objVertexData[0].length > 0) { + geometry.data.position = geometry.objVertexData[0]; + } + if (geometry.objVertexData[1].length > 0) { + geometry.data.texcoord = geometry.objVertexData[1]; + } + if (geometry.objVertexData[2].length > 0) { + geometry.data.normal = geometry.objVertexData[2]; + } + if (geometry.objVertexData[3].length > 0) { + geometry.data.color = geometry.objVertexData[3]; + } + } + + return { + geometries: Object.values(geometries), + }; +} +``` + +Затем нужно обновить шейдеры, чтобы они использовали цвета вершин: + +```js +const vs = `#version 300 es +in vec4 a_position; +in vec3 a_normal; +in vec2 a_texcoord; +in vec3 a_color; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; +uniform vec3 u_viewWorldPosition; + +out vec3 v_normal; +out vec3 v_surfaceToView; +out vec2 v_texcoord; +out vec3 v_color; + +void main() { + vec4 worldPosition = u_world * a_position; + gl_Position = u_projection * u_view * worldPosition; + v_surfaceToView = u_viewWorldPosition - worldPosition.xyz; + + v_normal = mat3(u_world) * a_normal; + v_texcoord = a_texcoord; + v_color = a_color; +} +`; + +const fs = `#version 300 es +precision highp float; + +in vec3 v_normal; +in vec3 v_surfaceToView; +in vec2 v_texcoord; +in vec3 v_color; + +uniform vec3 diffuse; +uniform sampler2D diffuseMap; +uniform vec3 ambient; +uniform vec3 emissive; +uniform vec3 specular; +uniform sampler2D specularMap; +uniform float shininess; +uniform float opacity; +uniform vec3 u_lightDirection; +uniform vec3 u_ambientLight; + +out vec4 outColor; + +void main () { + vec3 normal = normalize(v_normal); + + vec3 surfaceToViewDirection = normalize(v_surfaceToView); + vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection); + + float fakeLight = dot(u_lightDirection, normal) * .5 + .5; + float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0); + vec4 specularMapColor = texture(specularMap, v_texcoord); + vec3 effectiveSpecular = specular * specularMapColor.rgb; + + vec4 diffuseMapColor = texture(diffuseMap, v_texcoord); + vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color; + float effectiveOpacity = opacity * diffuseMapColor.a; + + outColor = vec4( + emissive + + ambient * u_ambientLight + + effectiveDiffuse * fakeLight + + effectiveSpecular * pow(specularLight, shininess), + effectiveOpacity); +} +`; +``` + +Также нужно обновить код, который создаёт буферы, чтобы он обрабатывал цвета вершин. +Наша [вспомогательная библиотека](webgl-less-code-more-fun.html) обрабатывает это для нас, если +мы установим данные для этого атрибута как `{value: [1, 2, 3, 4]}`. Итак, мы можем +проверить, если нет цветов вершин, то если так, установить атрибут цвета вершины +как константный белый. + +```js +const parts = obj.geometries.map(({data}) => { + // Потому что data - это просто именованные массивы, как это + // + // { + // position: [...], + // texcoord: [...], + // normal: [...], + // } + // + // и потому что эти имена соответствуют атрибутам в нашем вершинном + // шейдере, мы можем передать это напрямую в `createBufferInfoFromArrays` + // из статьи "less code more fun". + + if (data.color) { + if (data.position.length === data.color.length) { + // это 3. Наша вспомогательная библиотека предполагает 4, поэтому нам нужно + // сказать ей, что их только 3. + data.color = { numComponents: 3, data: data.color }; + } + } else { + // нет цветов вершин, поэтому просто используем константный белый + data.color = { value: [1, 1, 1, 1] }; + } + + // создаём буфер для каждого массива, вызывая + // gl.createBuffer, gl.bindBuffer, gl.bufferData + const bufferInfo = twgl.createBufferInfoFromArrays(gl, data); + const vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, bufferInfo); + return { + material: { + u_diffuse: [1, 1, 1, 1], + }, + bufferInfo, + vao, + }; +}); +``` + +И с этим мы можем загрузить .OBJ файл с цветами вершин. + +{{{example url="../webgl-load-obj-w-vertex-colors.html"}}} + +Что касается парсинга и использования материалов, [см. следующую статью](webgl-load-obj-w-mtl.html) \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-matrix-vs-math.md b/webgl/lessons/ru/webgl-matrix-vs-math.md index 9623a88c1..b0c0802e7 100644 --- a/webgl/lessons/ru/webgl-matrix-vs-math.md +++ b/webgl/lessons/ru/webgl-matrix-vs-math.md @@ -196,4 +196,38 @@ const someTranslationMatrix = [ Итак, с этим соглашением называть строки "столбцами" некоторые вещи проще, но другие могут быть более запутанными, если вы математик. -Я поднимаю все это, потому что эти статьи написаны с точки зрения программиста, а не математика. Это означает, что как и каждый другой одномерный массив, который обрабатывается как двумерный массив, строки идут поперек. \ No newline at end of file +Я поднимаю все это, потому что эти статьи написаны с точки зрения программиста, а не математика. Это означает, что как и каждый другой одномерный массив, который обрабатывается как двумерный массив, строки идут поперек. + +```js +const someTranslationMatrix = [ + 1, 0, 0, 0, // строка 0 + 0, 1, 0, 0, // строка 1 + 0, 0, 1, 0, // строка 2 + tx, ty, tz, 1, // строка 3 +]; +``` + +точно так же, как + +```js +// изображение смайлика +const dataFor7x8OneChannelImage = [ + 0, 255, 255, 255, 255, 255, 0, // строка 0 + 255, 0, 0, 0, 0, 0, 255, // строка 1 + 255, 0, 255, 0, 255, 0, 255, // строка 2 + 255, 0, 0, 0, 0, 0, 255, // строка 3 + 255, 0, 255, 0, 255, 0, 255, // строка 4 + 255, 0, 255, 255, 255, 0, 255, // строка 5 + 255, 0, 0, 0, 0, 0, 255, // строка 6 + 0, 255, 255, 255, 255, 255, 0, // строка 7 +] +``` + +и поэтому эти статьи будут ссылаться на них как на строки. + +Если вы математик, вы можете найти это запутанным. Мне жаль, что у меня +нет решения. Я мог бы назвать то, что явно является строкой 3, столбцом, +но это также было бы запутанно, поскольку это не соответствует никакому другому программированию. + +В любом случае, надеюсь, это помогает прояснить, почему ни одно из объяснений не выглядит как что-то из математической книги. Вместо этого они выглядят как код и используют соглашения кода. Я надеюсь, что это помогает объяснить, что происходит, +и это не слишком запутанно для тех, кто привык к математическим соглашениям. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-multiple-views.md b/webgl/lessons/ru/webgl-multiple-views.md index 67a47c58f..de1b580f7 100644 --- a/webgl/lessons/ru/webgl-multiple-views.md +++ b/webgl/lessons/ru/webgl-multiple-views.md @@ -160,8 +160,6 @@ const fieldOfViewRadians = degToRad(120); function render() { twgl.resizeCanvasToDisplaySize(gl.canvas); - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - gl.enable(gl.CULL_FACE); gl.enable(gl.DEPTH_TEST); @@ -185,6 +183,22 @@ function render() { // центрируем 'F' вокруг его начала worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 2; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем правый вид + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); } render(); @@ -197,4 +211,400 @@ render(); {{{example url="../webgl-multiple-views-one-view.html"}}} Теперь давайте сделаем так, чтобы он рисовал 2 вида 'F' бок о бок, -используя `gl.viewport` \ No newline at end of file +используя `gl.viewport` + +```js +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 2; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем правый вид + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); +} +render(); +``` + +{{{example url="../webgl-multiple-views.html"}}} + +Теперь у нас есть 2 вида 'F' бок о бок. Каждый вид использует половину canvas. + +Давайте добавим еще один вид, чтобы у нас было 3 вида + +```js +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 3; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем средний вид + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем правый вид + const left3 = width * 2; + const bottom3 = 0; + const width3 = width; + const height3 = height; + + gl.viewport(left3, bottom3, width3, height3); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); +} +render(); +``` + +{{{example url="../webgl-multiple-views-clear-fixed.html"}}} + +Теперь у нас есть 3 вида. Каждый вид использует треть canvas. + +Обратите внимание, что каждый вид рисует ту же сцену. Это может быть полезно для отладки или для создания интерфейса с множественными видами. + +Давайте также добавим scissor тест, чтобы убедиться, что мы не рисуем за пределами каждого viewport + +```js +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.SCISSOR_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 3; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + gl.scissor(left, bottom, width, height); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем средний вид + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); + gl.scissor(left2, bottom2, width2, height2); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем правый вид + const left3 = width * 2; + const bottom3 = 0; + const width3 = width; + const height3 = height; + + gl.viewport(left3, bottom3, width3, height3); + gl.scissor(left3, bottom3, width3, height3); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); +} +render(); +``` + +{{{example url="../webgl-multiple-views-clear-issue.html"}}} + +Теперь у нас есть scissor тест, который гарантирует, что мы не рисуем за пределами каждого viewport. + +Давайте также добавим очистку для каждого viewport + +```js +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.SCISSOR_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 3; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + gl.scissor(left, bottom, width, height); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем средний вид + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); + gl.scissor(left2, bottom2, width2, height2); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем правый вид + const left3 = width * 2; + const bottom3 = 0; + const width3 = width; + const height3 = height; + + gl.viewport(left3, bottom3, width3, height3); + gl.scissor(left3, bottom3, width3, height3); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); +} +render(); +``` + +{{{example url="../webgl-multiple-views-clear-fixed.html"}}} + +Теперь у нас есть очистка для каждого viewport, что гарантирует, что каждый вид имеет чистый фон. + +Давайте также добавим возможность рисовать разные сцены в каждом viewport + +```js +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.SCISSOR_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + // рисуем левый вид + const left = 0; + const bottom = 0; + const width = gl.canvas.width / 3; + const height = gl.canvas.height; + + gl.viewport(left, bottom, width, height); + gl.scissor(left, bottom, width, height); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем средний вид с другой камерой + const left2 = width; + const bottom2 = 0; + const width2 = width; + const height2 = height; + + gl.viewport(left2, bottom2, width2, height2); + gl.scissor(left2, bottom2, width2, height2); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // используем другую камеру для среднего вида + const cameraPosition2 = [0, 0, -50]; + const cameraMatrix2 = m4.lookAt(cameraPosition2, target, up); + drawScene(perspectiveProjectionMatrix, cameraMatrix2, worldMatrix); + + // рисуем правый вид с другой камерой + const left3 = width * 2; + const bottom3 = 0; + const width3 = width; + const height3 = height; + + gl.viewport(left3, bottom3, width3, height3); + gl.scissor(left3, bottom3, width3, height3); + gl.clearColor(0.2, 0.2, 0.2, 1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // используем другую камеру для правого вида + const cameraPosition3 = [0, 0, -100]; + const cameraMatrix3 = m4.lookAt(cameraPosition3, target, up); + drawScene(perspectiveProjectionMatrix, cameraMatrix3, worldMatrix); +} +render(); +``` + +{{{example url="../webgl-multiple-views-items.html"}}} + +Теперь у нас есть 3 вида с разными камерами. Каждый вид показывает ту же сцену с разных ракурсов. + +Конечно, вы могли бы рисовать целые 3D сцены или что угодно для каждого элемента. +Пока вы правильно устанавливаете viewport и scissor, а затем настраиваете +вашу матрицу проекции, чтобы соответствовать аспекту области, это должно работать. + +Еще одна примечательная вещь о коде - мы перемещаем canvas +с этой строкой + +``` +gl.canvas.style.transform = `translateY(${window.scrollY}px)`; +``` + +Почему? Мы могли бы вместо этого установить canvas в `position: fixed;`, в этом случае +он не прокручивался бы со страницей. Разница была бы тонкой. +Браузер пытается прокручивать страницу как можно более плавно. Это может быть +быстрее, чем мы можем рисовать наши объекты. Из-за этого у нас есть 2 варианта. + +1. Использовать canvas с фиксированной позицией + + В этом случае, если мы не можем обновляться достаточно быстро, HTML перед canvas будет прокручиваться, но сам canvas + не будет, поэтому на несколько мгновений они будут не синхронизированы + + + +2. Перемещать canvas под контентом + + В этом случае, если мы не можем обновляться достаточно быстро, canvas будет прокручиваться в синхронизации + с HTML, но новые области, где мы хотим рисовать вещи, будут пустыми, пока мы не получим + шанс нарисовать. + + + + Это решение, используемое выше + +Надеюсь, эта статья дала вам некоторые идеи о том, как рисовать множественные виды. +Мы будем использовать эти техники в нескольких будущих статьях, где +возможность видеть множественные виды полезна для понимания. + +
+

Координаты пикселей

+

Координаты пикселей в WebGL +ссылаются на их края. Так, например, если у нас был +canvas размером 3x2 пикселя, и мы установили viewport +как

+

+gl.viewport(
+  0, // left
+  0, // bottom
+  3, // width
+  2, // height
+);
+
+

Тогда мы действительно определяем этот прямоугольник, который окружает 3x2 пикселя

+
+

Это означает, что значение пространства отсечения X = -1.0 соответствует левому краю этого прямоугольника, +а значение пространства отсечения X = 1.0 соответствует правому. Выше я сказал, что X = -1.0 соответствует самому левому пикселю, +но на самом деле соответствует левому краю

+
diff --git a/webgl/lessons/ru/webgl-picking.md b/webgl/lessons/ru/webgl-picking.md index fdb830946..53a2794fe 100644 --- a/webgl/lessons/ru/webgl-picking.md +++ b/webgl/lessons/ru/webgl-picking.md @@ -1,56 +1,61 @@ -Title: WebGL2 Пикинг (выбор объектов) +Title: WebGL2 Выбор объектов Description: Как выбирать объекты в WebGL -TOC: Пикинг (клик по объектам) +TOC: Выбор объектов (клик по объектам) -Эта статья о том, как использовать WebGL, чтобы позволить пользователю выбирать или выделять объекты. +Эта статья о том, как использовать WebGL для того, чтобы пользователь мог выбирать или выделять +объекты. -Если вы читали другие статьи на этом сайте, вы, вероятно, уже поняли, -что сам WebGL — это просто библиотека растеризации. Он рисует треугольники, -линии и точки на canvas, поэтому у него нет понятия «объекты для выбора». -Он просто выводит пиксели через ваши шейдеры. Это значит, -что любая концепция «пикинга» должна реализовываться в вашем коде. Вы должны -определить, что это за объекты, которые пользователь может выбрать. -То есть, хотя эта статья может охватить общие концепции, вам нужно будет -самостоятельно решить, как применить их в вашем приложении. +Если вы читали другие статьи на этом сайте, вы, надеюсь, поняли, +что WebGL сам по себе - это просто библиотека растеризации. Он рисует треугольники, +линии и точки на canvas, поэтому у него нет концепции "объектов для +выбора". Он просто выводит пиксели через шейдеры, которые вы предоставляете. Это означает, +что любая концепция "выбора" чего-либо должна исходить из вашего кода. Вам нужно +определить, что это за вещи, которые вы позволяете пользователю выбирать. +Это означает, что хотя эта статья может охватывать общие концепции, вам нужно будет +самостоятельно решить, как перевести то, что вы видите здесь, в применимые +концепции в вашем собственном приложении. ## Клик по объекту -Один из самых простых способов определить, по какому объекту кликнул пользователь — -присвоить каждому объекту числовой id, затем отрисовать +Один из самых простых способов выяснить, на какую вещь кликнул пользователь, это +придумать числовой id для каждого объекта, затем мы можем нарисовать все объекты, используя их id как цвет, без освещения -и текстур. Это даст нам изображение силуэтов -каждого объекта. Буфер глубины сам отсортирует объекты. -Затем мы можем считать цвет пикселя под мышью — это даст нам id объекта, который был отрисован в этой точке. +и без текстур. Это даст нам изображение силуэтов +каждого объекта. Буфер глубины будет обрабатывать сортировку за нас. +Затем мы можем прочитать цвет пикселя под +мышью, который даст нам id объекта, который был отрендерен там. -Чтобы реализовать этот метод, нам нужно объединить несколько предыдущих -статей. Первая — [о рисовании множества объектов](webgl-drawing-multiple-things.html), -потому что она показывает, как рисовать много объектов, которые мы и будем выбирать. +Для реализации этой техники нам нужно будет объединить несколько предыдущих +статей. Первая - это статья о [рисовании множественных объектов](webgl-drawing-multiple-things.html), +которую мы будем использовать, потому что, учитывая, что она рисует множество вещей, мы можем попытаться +выбрать их. -Обычно мы хотим рендерить эти id вне экрана — -[рендеря в текстуру](webgl-render-to-texture.html), так что добавим и этот код. +Помимо этого, мы обычно хотим рендерить эти id вне экрана, +[рендеря в текстуру](webgl-render-to-texture.html), поэтому мы +также добавим этот код. -Начнем с последнего примера из -[статьи о рисовании множества объектов](webgl-drawing-multiple-things.html), -который рисует 200 объектов. +Итак, давайте начнем с последнего примера из +[статьи о рисовании множественных вещей](webgl-drawing-multiple-things.html), +которая рисует 200 объектов. -Добавим к нему framebuffer с прикреплённой текстурой и depth-буфером из -последнего примера в [статье о рендере в текстуру](webgl-render-to-texture.html). +К нему давайте добавим framebuffer с присоединенной текстурой и буфером глубины из +последнего примера в [статье о рендеринге в текстуру](webgl-render-to-texture.html). ```js -// Создаем текстуру для рендера +// Создаем текстуру для рендеринга const targetTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, targetTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); -// создаем depth renderbuffer +// создаем буфер глубины const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); function setFramebufferAttachmentSizes(width, height) { gl.bindTexture(gl.TEXTURE_2D, targetTexture); - // задаем размер и формат уровня 0 + // определяем размер и формат уровня 0 const level = 0; const internalFormat = gl.RGBA; const border = 0; @@ -65,41 +70,43 @@ function setFramebufferAttachmentSizes(width, height) { gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); } -// Создаем и биндим framebuffer +// Создаем и привязываем framebuffer const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); -// прикрепляем текстуру как первый цветовой attachment +// присоединяем текстуру как первое цветовое вложение const attachmentPoint = gl.COLOR_ATTACHMENT0; const level = 0; gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level); -// создаем depth-буфер такого же размера, как targetTexture +// делаем буфер глубины того же размера, что и targetTexture gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); ``` -Мы вынесли код установки размеров текстуры и depth renderbuffer в функцию, чтобы -можно было вызывать её при изменении размера canvas. +Мы поместили код для установки размеров текстуры и +буфера глубины в функцию, чтобы мы могли +вызывать ее для изменения их размера в соответствии с размером +canvas. -В рендер-цикле, если размер canvas изменился, -мы подгоним текстуру и renderbuffer под новые размеры. +В нашем коде рендеринга, если canvas изменяет размер, +мы скорректируем текстуру и renderbuffer, чтобы они соответствовали. ```js function drawScene(time) { time *= 0.0005; -- webglUtils.resizeCanvasToDisplaySize(gl.canvas); -+ if (webglUtils.resizeCanvasToDisplaySize(gl.canvas)) { -+ // canvas изменился, подгоняем framebuffer attachments -+ setFramebufferAttachmentSizes(gl.canvas.width, gl.canvas.height); -+ } + if (webglUtils.resizeCanvasToDisplaySize(gl.canvas)) { + // canvas был изменен, делаем вложения framebuffer соответствующими + setFramebufferAttachmentSizes(gl.canvas.width, gl.canvas.height); + } ... ``` -Далее нам нужен второй шейдер. В примере используется рендер по цветам вершин, но нам нужен -шейдер, который будет рисовать сплошным цветом (id). -Вот наш второй шейдер: +Далее нам нужен второй шейдер. Шейдер в +примере рендерит, используя цвета вершин, но нам нужен +тот, который мы можем установить в сплошной цвет для рендеринга с id. +Итак, сначала вот наш второй шейдер ```js const pickingVS = `#version 300 es @@ -126,13 +133,13 @@ const pickingFS = `#version 300 es `; ``` -И нам нужно скомпилировать, связать и найти локации -используя наши [хелперы](webgl-less-code-more-fun.html). +И нам нужно скомпилировать, связать и найти местоположения, +используя наши [помощники](webgl-less-code-more-fun.html). ```js -// настройка GLSL программ -// важно: нам нужно, чтобы атрибуты совпадали между программами -// чтобы можно было использовать один и тот же vertex array для разных шейдеров +// настройка GLSL программы +// примечание: нам нужны позиции атрибутов, чтобы соответствовать между программами +// чтобы нам нужен был только один vertex array на форму const options = { attribLocations: { a_position: 0, @@ -143,15 +150,19 @@ const programInfo = twgl.createProgramInfo(gl, [vs, fs], options); const pickingProgramInfo = twgl.createProgramInfo(gl, [pickingVS, pickingFS], options); ``` -В отличие от большинства примеров на сайте, здесь нам нужно рисовать одни и те же данные двумя разными шейдерами. -Поэтому нам нужно, чтобы локации атрибутов совпадали между шейдерами. Это можно сделать двумя способами. Первый — явно указать их в GLSL: +Одно отличие выше от большинства примеров на этом сайте, это один +из немногих случаев, когда нам нужно было рисовать те же данные с 2 разными +шейдерами. Из-за этого нам нужны местоположения атрибутов, чтобы соответствовать +между шейдерами. Мы можем сделать это 2 способами. Один способ - установить их +вручную в GLSL ```glsl layout (location = 0) in vec4 a_position; layout (location = 1) in vec4 a_color; ``` -Второй — вызвать `gl.bindAttribLocation` **до** линковки программы: +Другой - вызвать `gl.bindAttribLocation` **до** связывания +шейдерной программы ```js gl.bindAttribLocation(someProgram, 0, 'a_position'); @@ -159,15 +170,21 @@ gl.bindAttribLocation(someProgram, 1, 'a_color'); gl.linkProgram(someProgram); ``` -Этот способ нечасто используется, но он более +Этот последний стиль необычен, но он более [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). -Наша хелпер-библиотека вызывает `gl.bindAttribLocation` за нас, -если мы передаем имена атрибутов и нужные локации — это и происходит выше. +Наша библиотека помощников вызовет `gl.bindAttribLocation` для нас, +если мы передадим имена атрибутов и местоположение, которое мы хотим, +что и происходит выше. -Это гарантирует, что атрибут `a_position` будет использовать локацию 0 в обеих программах, так что мы можем использовать один и тот же vertex array. +Это означает, что мы можем гарантировать, что атрибут `a_position` использует +местоположение 0 в обеих программах, поэтому мы можем использовать тот же vertex array +с обеими программами. -Далее нам нужно уметь рендерить все объекты дважды: сначала обычным шейдером, потом — только что написанным. -Вынесем код рендера всех объектов в функцию. +Далее нам нужно иметь возможность рендерить все объекты +дважды. Один раз с любым шейдером, который мы назначили +им, и снова с шейдером, который мы только что написали, +поэтому давайте извлечем код, который в настоящее время рендерит +все объекты в функцию. ```js function drawObjects(objectsToDraw, overrideProgramInfo) { @@ -179,4 +196,527 @@ function drawObjects(objectsToDraw, overrideProgramInfo) { gl.useProgram(programInfo.program); // Настраиваем все нужные атрибуты. - gl.bindVertexArray(vertexArray); \ No newline at end of file + gl.bindVertexArray(vertexArray); + + // Устанавливаем uniforms. + twgl.setUniforms(programInfo, object.uniforms); + + // Рисуем (вызывает gl.drawArrays или gl.drawElements) + twgl.drawBufferInfo(gl, object.bufferInfo); + }); +} +``` + +`drawObjects` принимает опциональный `overrideProgramInfo`, +который мы можем передать, чтобы использовать наш picking шейдер вместо +назначенного объекту шейдера. + +Давайте вызовем его один раз, чтобы нарисовать в текстуру с +id, и снова, чтобы нарисовать сцену на canvas. + +```js +// Рисуем сцену. +function drawScene(time) { + time *= 0.0005; + + ... + + // Вычисляем матрицы для каждого объекта. + objects.forEach(function(object) { + object.uniforms.u_matrix = computeMatrix( + viewProjectionMatrix, + object.translation, + object.xRotationSpeed * time, + object.yRotationSpeed * time); + }); + ++ // ------ Рисуем объекты в текстуру -------- ++ ++ gl.bindFramebuffer(gl.FRAMEBUFFER, fb); ++ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); ++ ++ gl.enable(gl.CULL_FACE); ++ gl.enable(gl.DEPTH_TEST); ++ ++ // Очищаем canvas И буфер глубины. ++ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); ++ ++ drawObjects(objectsToDraw, pickingProgramInfo); ++ ++ // ------ Рисуем объекты на canvas + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + drawObjects(objectsToDraw); + + requestAnimationFrame(drawScene); +} +``` + +И с этим мы должны иметь возможность двигать мышью по +сцене, и объект под мышью будет мигать + +{{{example url="../webgl-picking-w-gpu.html" }}} + +Одна оптимизация, которую мы можем сделать, мы рендерим +id в текстуру того же размера, +что и canvas. Это концептуально самая простая +вещь для выполнения. + +Но мы могли бы вместо этого просто рендерить пиксель +под мышью. Для этого мы используем усеченную пирамиду, +математика которой будет покрывать только пространство для этого +1 пикселя. + +До сих пор для 3D мы использовали функцию под названием +`perspective`, которая принимает в качестве входных данных поле зрения, соотношение сторон и +ближнее и дальнее значения для z-плоскостей и создает +матрицу перспективной проекции, которая преобразует из +усеченной пирамиды, определенной этими значениями, в clip space. + +Большинство 3D математических библиотек имеют другую функцию под названием +`frustum`, которая принимает 6 значений, левое, правое, верхнее, +и нижнее значения для ближней z-плоскости, а затем +z-ближнее и z-дальнее значения для z-плоскостей и генерирует +матрицу перспективы, определенную этими значениями. + +Используя это, мы можем сгенерировать матрицу перспективы для +одного пикселя под мышью + +Сначала мы вычисляем края и размер того, чем была бы наша ближняя плоскость, +если бы мы использовали функцию `perspective` + +```js +// вычисляем прямоугольник, который покрывает ближняя плоскость нашей усеченной пирамиды +const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; +const top = Math.tan(fieldOfViewRadians * 0.5) * near; +const bottom = -top; +const left = aspect * bottom; +const right = aspect * top; +const width = Math.abs(right - left); +const height = Math.abs(top - bottom); +``` + +Итак, `left`, `right`, `width` и `height` - это +размер и позиция ближней плоскости. Теперь на этой +плоскости мы можем вычислить размер и позицию +одного пикселя под мышью и передать это в +функцию `frustum` для генерации матрицы проекции, +которая покрывает только этот 1 пиксель + +```js +// вычисляем часть ближней плоскости, которая покрывает 1 пиксель +// под мышью. +const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth; +const pixelY = gl.canvas.height - mouseY * gl.canvas.height / gl.canvas.clientHeight - 1; + +const subLeft = left + pixelX * width / gl.canvas.width; +const subBottom = bottom + pixelY * height / gl.canvas.height; +const subWidth = width / gl.canvas.width; +const subHeight = height / gl.canvas.height; + +// делаем усеченную пирамиду для этого 1 пикселя +const projectionMatrix = m4.frustum( + subLeft, + subLeft + subWidth, + subBottom, + subBottom + subHeight, + near, + far); +``` + +Для использования этого нам нужно внести некоторые изменения. Как сейчас наш шейдер +просто принимает `u_matrix`, что означает, что для рисования с другой +матрицей проекции нам нужно будет пересчитывать матрицы для каждого объекта +дважды каждый кадр, один раз с нашей нормальной матрицей проекции для рисования +на canvas и снова для этой матрицы проекции 1 пикселя. + +Мы можем убрать эту ответственность из JavaScript, переместив это +умножение в вершинные шейдеры. + +```html +const vs = `#version 300 es + +in vec4 a_position; +in vec4 a_color; + +-uniform mat4 u_matrix; ++uniform mat4 u_viewProjection; ++uniform mat4 u_world; + +out vec4 v_color; + +void main() { + // Умножаем позицию на матрицу. +- gl_Position = u_matrix * a_position; ++ gl_Position = u_viewProjection * u_world * a_position; + + // Передаем цвет в фрагментный шейдер. + v_color = a_color; +} +`; + +... + +const pickingVS = `#version 300 es + in vec4 a_position; + +- uniform mat4 u_matrix; ++ uniform mat4 u_viewProjection; ++ uniform mat4 u_world; + + void main() { + // Умножаем позицию на матрицу. +- gl_Position = u_matrix * a_position; ++ gl_Position = u_viewProjection * u_world * a_position; + } +`; +``` + +Затем мы можем сделать наш JavaScript `viewProjectionMatrix` общим +среди всех объектов. + +```js +const objectsToDraw = []; +const objects = []; +const viewProjectionMatrix = m4.identity(); + +// Создаем информацию для каждого объекта для каждого объекта. +const baseHue = rand(0, 360); +const numObjects = 200; +for (let ii = 0; ii < numObjects; ++ii) { + const id = ii + 1; + + // выбираем форму + const shape = shapes[rand(shapes.length) | 0]; + + const object = { + uniforms: { + u_colorMult: chroma.hsv(eMod(baseHue + rand(0, 120), 360), rand(0.5, 1), rand(0.5, 1)).gl(), + u_world: m4.identity(), + u_viewProjection: viewProjectionMatrix, + u_id: [ + ((id >> 0) & 0xFF) / 0xFF, + ((id >> 8) & 0xFF) / 0xFF, + ((id >> 16) & 0xFF) / 0xFF, + ((id >> 24) & 0xFF) / 0xFF, + ], + }, + translation: [rand(-100, 100), rand(-100, 100), rand(-150, -50)], + xRotationSpeed: rand(0.8, 1.2), + yRotationSpeed: rand(0.8, 1.2), + }; + objects.push(object); + + // Добавляем его в список вещей для рисования. + objectsToDraw.push({ + programInfo: programInfo, + bufferInfo: shape.bufferInfo, + vertexArray: shape.vertexArray, + uniforms: object.uniforms, + }); +} +``` + +И где мы вычисляем матрицы для каждого объекта, нам больше не нужно +включать матрицу проекции вида + +```js +function computeMatrix(translation, xRotation, yRotation) { + let matrix = m4.translation( + translation[0], + translation[1], + translation[2]); + matrix = m4.xRotate(matrix, xRotation); + return m4.yRotate(matrix, yRotation); +} +``` + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + drawObjects(objectsToDraw); + + requestAnimationFrame(drawScene); +} +``` + +Нашему picking шейдеру нужен `u_id`, установленный в id, поэтому давайте +добавим это к нашим данным uniform, где мы настраиваем наши объекты. + +```js +// Создаем информацию для каждого объекта для каждого объекта. +const baseHue = rand(0, 360); +const numObjects = 200; +for (let ii = 0; ii < numObjects; ++ii) { + const id = ii + 1; + + // выбираем форму + const shape = shapes[rand(shapes.length) | 0]; + + const object = { + uniforms: { + u_colorMult: chroma.hsv(eMod(baseHue + rand(0, 120), 360), rand(0.5, 1), rand(0.5, 1)).gl(), + u_matrix: m4.identity(), + u_id: [ + ((id >> 0) & 0xFF) / 0xFF, + ((id >> 8) & 0xFF) / 0xFF, + ((id >> 16) & 0xFF) / 0xFF, + ((id >> 24) & 0xFF) / 0xFF, + ], + }, + translation: [rand(-100, 100), rand(-100, 100), rand(-150, -50)], + xRotationSpeed: rand(0.8, 1.2), + yRotationSpeed: rand(0.8, 1.2), + }; + objects.push(object); + + // Добавляем его в список вещей для рисования. + objectsToDraw.push({ + programInfo: programInfo, + bufferInfo: shape.bufferInfo, + vertexArray: shape.vertexArray, + uniforms: object.uniforms, + }); +} +``` + +Это будет работать, потому что наша [библиотека помощников](webgl-less-code-more-fun.html) +обрабатывает применение uniforms для нас. + +Нам пришлось разделить id по R, G, B и A. Потому что формат/тип нашей +текстуры - `gl.RGBA`, `gl.UNSIGNED_BYTE`, +мы получаем 8 бит на канал. 8 бит представляют только 256 значений, +но, разделив id по 4 каналам, мы получаем 32 бита всего, +что составляет > 4 миллиарда значений. + +Мы добавляем 1 к id, потому что мы будем использовать 0 для обозначения +"ничего под мышью". + +Теперь давайте выделим объект под мышью. + +Сначала нам нужен код для получения позиции мыши относительно canvas. + +```js +// mouseX и mouseY находятся в CSS display space относительно canvas +let mouseX = -1; +let mouseY = -1; + +... + +gl.canvas.addEventListener('mousemove', (e) => { + const rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.left; + mouseY = e.clientY - rect.top; +}); +``` + +Обратите внимание, что с кодом выше `mouseX` и `mouseY` +находятся в CSS пикселях в display space. Это означает, +что они находятся в пространстве, где отображается canvas, +а не в пространстве того, сколько пикселей в canvas. +Другими словами, если у вас был canvas как этот + +```html + +``` + +тогда `mouseX` будет идти от 0 до 33 по canvas и +`mouseY` будет идти от 0 до 44 по canvas. Смотрите [это](webgl-resizing-the-canvas.html) +для получения дополнительной информации. + +Теперь, когда у нас есть позиция мыши, давайте добавим код +для поиска пикселя под мышью + +```js +const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth; +const pixelY = gl.canvas.height - mouseY * gl.canvas.height / gl.canvas.clientHeight - 1; +const data = new Uint8Array(4); +gl.readPixels( + pixelX, // x + pixelY, // y + 1, // width + 1, // height + gl.RGBA, // format + gl.UNSIGNED_BYTE, // type + data); // typed array to hold result +const id = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24); +``` + +Код выше, который вычисляет `pixelX` и `pixelY`, преобразует +из `mouseX` и `mouseY` в display space в пиксели в пространстве canvas. +Другими словами, учитывая пример выше, где `mouseX` шел от +0 до 33 и `mouseY` шел от 0 до 44. `pixelX` будет идти от 0 до 11 +и `pixelY` будет идти от 0 до 22. + +В нашем фактическом коде мы используем нашу утилитную функцию `resizeCanvasToDisplaySize` +и мы делаем нашу текстуру того же размера, что и canvas, поэтому display +размер и размер canvas совпадают, но, по крайней мере, мы готовы к случаю, +когда они не совпадают. + +Теперь, когда у нас есть id, чтобы фактически выделить выбранный объект, +давайте изменим цвет, который мы используем для его рендеринга на canvas. +Шейдер, который мы использовали, имеет uniform `u_colorMult`, +который мы можем использовать, поэтому если объект под мышью, мы найдем его, +сохраним его значение `u_colorMult`, заменим его цветом выделения, +и восстановим его. + +```js +// mouseX и mouseY находятся в CSS display space относительно canvas +let mouseX = -1; +let mouseY = -1; +let oldPickNdx = -1; +let oldPickColor; +let frameCount = 0; + +// Рисуем сцену. +function drawScene(time) { + time *= 0.0005; + ++frameCount; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); +``` + +Перед рендерингом id вне экрана мы устанавливаем матрицу проекции вида, +используя нашу матрицу проекции для 1 пикселя, а при рисовании на canvas +используем исходную матрицу проекции. + +```js +// Вычисляем матрицу камеры с помощью lookAt. +const cameraPosition = [0, 0, 100]; +const target = [0, 0, 0]; +const up = [0, 1, 0]; +const cameraMatrix = m4.lookAt(cameraPosition, target, up); + +// Создаём матрицу вида из матрицы камеры. +const viewMatrix = m4.inverse(cameraMatrix); + +// Вычисляем матрицы для каждого объекта. +objects.forEach(function(object) { + object.uniforms.u_world = computeMatrix( + object.translation, + object.xRotationSpeed * time, + object.yRotationSpeed * time); +}); + +// ------ Рисуем объекты в текстуру -------- + +// Определяем, какой пиксель под мышью, и настраиваем +// усечённую пирамиду для рендера только этого пикселя + +{ + // вычисляем прямоугольник, который покрывает ближнюю плоскость нашей усечённой пирамиды + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const top = Math.tan(fieldOfViewRadians * 0.5) * near; + const bottom = -top; + const left = aspect * bottom; + const right = aspect * top; + const width = Math.abs(right - left); + const height = Math.abs(top - bottom); + + // вычисляем часть ближней плоскости, которая покрывает 1 пиксель под мышью + const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth; + const pixelY = gl.canvas.height - mouseY * gl.canvas.height / gl.canvas.clientHeight - 1; + + const subLeft = left + pixelX * width / gl.canvas.width; + const subBottom = bottom + pixelY * height / gl.canvas.height; + const subWidth = width / gl.canvas.width; + const subHeight = height / gl.canvas.height; + + // создаём усечённую пирамиду для этого 1 пикселя + const projectionMatrix = m4.frustum( + subLeft, + subLeft + subWidth, + subBottom, + subBottom + subHeight, + near, + far); + m4.multiply(projectionMatrix, viewMatrix, viewProjectionMatrix); +} + +gl.bindFramebuffer(gl.FRAMEBUFFER, fb); +gl.viewport(0, 0, 1, 1); + +gl.enable(gl.CULL_FACE); +gl.enable(gl.DEPTH_TEST); + +gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + +drawObjects(objectsToDraw, pickingProgramInfo); + +// читаем 1 пиксель +const data = new Uint8Array(4); +gl.readPixels( + 0, // x + 0, // y + 1, // width + 1, // height + gl.RGBA, // format + gl.UNSIGNED_BYTE, // type + data); // typed array to hold result +const id = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24); + +// восстанавливаем цвет объекта +if (oldPickNdx >= 0) { + const object = objects[oldPickNdx]; + object.uniforms.u_colorMult = oldPickColor; + oldPickNdx = -1; +} + +// выделяем объект под мышью +if (id > 0) { + const pickNdx = id - 1; + oldPickNdx = pickNdx; + const object = objects[pickNdx]; + oldPickColor = object.uniforms.u_colorMult; + object.uniforms.u_colorMult = (frameCount & 0x8) ? [1, 0, 0, 1] : [1, 1, 0, 1]; +} + +// ------ Рисуем объекты на canvas + +{ + // Вычисляем матрицу проекции + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + m4.multiply(projectionMatrix, viewMatrix, viewProjectionMatrix); +} + +gl.bindFramebuffer(gl.FRAMEBUFFER, null); +gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + +drawObjects(objectsToDraw); + +requestAnimationFrame(drawScene); +} +``` + +Как видно, математика работает: мы рендерим только один пиксель +и всё равно определяем, что находится под мышью. + +{{{example url="../webgl-picking-w-gpu-1pixel.html"}}} + +Эта оптимизация может быть полезна, если у вас много объектов +и вы хотите минимизировать использование памяти. Вместо создания +текстуры размером с canvas, вы создаете текстуру размером 1x1 пиксель. + +Но есть компромисс. Теперь мы должны вычислять усеченную пирамиду +для каждого пикселя, что может быть дороже, чем просто создание +большей текстуры. Это зависит от вашего случая использования. + +Также обратите внимание, что мы больше не читаем пиксель из позиции +мыши. Мы читаем пиксель из позиции (0,0), потому что теперь мы +рендерим только 1 пиксель в позиции (0,0) нашей 1x1 текстуры. + +Это одна из многих техник, которые вы можете использовать для выбора +объектов в WebGL. Другие включают: + +1. **Ray casting** - бросание луча из позиции мыши в 3D пространство +2. **Bounding box/sphere testing** - проверка, находится ли точка внутри ограничивающего прямоугольника/сферы +3. **GPU picking** - то, что мы только что сделали +4. **Hierarchical picking** - выбор на основе иерархии объектов + +Каждая техника имеет свои преимущества и недостатки в зависимости +от вашего случая использования. diff --git a/webgl/lessons/ru/webgl-planar-projection-mapping.md b/webgl/lessons/ru/webgl-planar-projection-mapping.md index 26a01d169..538f653ce 100644 --- a/webgl/lessons/ru/webgl-planar-projection-mapping.md +++ b/webgl/lessons/ru/webgl-planar-projection-mapping.md @@ -45,14 +45,20 @@ in vec2 a_texcoord; uniform mat4 u_projection; uniform mat4 u_view; uniform mat4 u_world; +uniform mat4 u_textureMatrix; out vec2 v_texcoord; +out vec4 v_projectedTexcoord; void main() { - gl_Position = u_projection * u_view * u_world * a_position; + vec4 worldPosition = u_world * a_position; + + gl_Position = u_projection * u_view * worldPosition; // Передаём текстурные координаты во фрагментный шейдер. v_texcoord = a_texcoord; + + v_projectedTexcoord = u_textureMatrix * worldPosition; } `; ``` @@ -66,14 +72,29 @@ precision highp float; // Передано из вершинного шейдера. in vec2 v_texcoord; +in vec4 v_projectedTexcoord; uniform vec4 u_colorMult; uniform sampler2D u_texture; +uniform sampler2D u_projectedTexture; out vec4 outColor; void main() { - outColor = texture(u_texture, v_texcoord) * u_colorMult; + // делим на w, чтобы получить правильное значение. См. статью о перспективе + vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w; + + bool inRange = + projectedTexcoord.x >= 0.0 && + projectedTexcoord.x <= 1.0 && + projectedTexcoord.y >= 0.0 && + projectedTexcoord.y <= 1.0; + + vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy); + vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult; + + float projectedAmount = inRange ? 1.0 : 0.0; + outColor = mix(texColor, projectedTexColor, projectedAmount); } `; ``` @@ -192,4 +213,481 @@ function drawScene(projectionMatrix, cameraMatrix) { ```js const settings = { cameraX: 2.75, - cameraY: 5, \ No newline at end of file + cameraY: 5, +}; +const fieldOfViewRadians = degToRad(60); + +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + // Говорим WebGL, как конвертировать из clip space в пиксели + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + // Очищаем canvas И буфер глубины. + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // Вычисляем матрицу проекции + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, 1, 2000); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [settings.cameraX, settings.cameraY, 7]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + drawScene(projectionMatrix, cameraMatrix); +} +render(); +``` + +Теперь у нас есть простая сцена с плоскостью и сферой. +Я добавил несколько слайдеров, чтобы вы могли изменить позицию камеры +и лучше понять сцену. + +{{{example url="../webgl-planar-projection-setup.html"}}} + +Теперь давайте планарно спроецируем текстуру на сферу и плоскость. + +Первое, что нужно сделать — [загрузить текстуру](webgl-3d-textures.html). + +```js +function loadImageTexture(url) { + // Создаём текстуру. + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + // Заполняем текстуру 1x1 синим пикселем. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255])); + // Асинхронно загружаем изображение + const image = new Image(); + image.src = url; + image.addEventListener('load', function() { + // Теперь, когда изображение загружено, копируем его в текстуру. + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + // предполагаем, что эта текстура имеет размер степени 2 + gl.generateMipmap(gl.TEXTURE_2D); + render(); + }); + return texture; +} + +const imageTexture = loadImageTexture('resources/f-texture.png'); +``` + +Вспомним из [статьи о визуализации камеры](webgl-visualizing-the-camera.html), +мы создали куб от -1 до +1 и нарисовали его, чтобы представить усечённую пирамиду камеры. +Наши матрицы сделали так, что пространство внутри этой пирамиды представляет некоторую +область в форме усечённой пирамиды в мировом пространстве, которая преобразуется +из этого мирового пространства в clip space от -1 до +1. Мы можем сделать аналогичную вещь здесь. + +Давайте попробуем. Сначала в нашем фрагментном шейдере мы будем рисовать спроецированную текстуру +везде, где её текстурные координаты находятся между 0.0 и 1.0. +За пределами этого диапазона мы будем использовать текстуру-шахматку. + +```js +const fs = `#version 300 es +precision highp float; + +// Передано из вершинного шейдера. +in vec2 v_texcoord; +in vec4 v_projectedTexcoord; + +uniform vec4 u_colorMult; +uniform sampler2D u_texture; +uniform sampler2D u_projectedTexture; + +out vec4 outColor; + +void main() { + // делим на w, чтобы получить правильное значение. См. статью о перспективе + vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w; + + bool inRange = + projectedTexcoord.x >= 0.0 && + projectedTexcoord.x <= 1.0 && + projectedTexcoord.y >= 0.0 && + projectedTexcoord.y <= 1.0; + + vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy); + vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult; + + float projectedAmount = inRange ? 1.0 : 0.0; + outColor = mix(texColor, projectedTexColor, projectedAmount); +} +`; +``` + +Для вычисления спроецированных текстурных координат мы создадим +матрицу, которая представляет 3D пространство, ориентированное и позиционированное +в определённом направлении, точно так же, как камера из [статьи о визуализации камеры](webgl-visualizing-the-camera.html). +Затем мы спроецируем мировые позиции +вершин сферы и плоскости через это пространство. Там, где +они находятся между 0 и 1, код, который мы только что написали, покажет +текстуру. + +Давайте добавим код в вершинный шейдер для проецирования мировых позиций +сферы и плоскости через это *пространство*. + +```js +const vs = `#version 300 es +in vec4 a_position; +in vec2 a_texcoord; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; +uniform mat4 u_textureMatrix; + +out vec2 v_texcoord; +out vec4 v_projectedTexcoord; + +void main() { + vec4 worldPosition = u_world * a_position; + + gl_Position = u_projection * u_view * worldPosition; + + // Передаём текстурные координаты во фрагментный шейдер. + v_texcoord = a_texcoord; + + v_projectedTexcoord = u_textureMatrix * worldPosition; +} +`; +``` + +Теперь всё, что осталось — это фактически вычислить матрицу, которая +определяет это ориентированное пространство. Всё, что нам нужно сделать — это вычислить +мировую матрицу, как мы бы делали для любого другого объекта, а затем взять +её обратную. Это даст нам матрицу, которая позволяет нам ориентировать +мировые позиции других объектов относительно этого пространства. +Это точно то же самое, что делает матрица вида из +[статьи о камерах](webgl-3d-camera.html). + +Мы будем использовать нашу функцию `lookAt`, которую мы создали в [той же статье](webgl-3d-camera.html). + +```js +const settings = { + cameraX: 2.75, + cameraY: 5, + posX: 3.5, + posY: 4.4, + posZ: 4.7, + targetX: 0.8, + targetY: 0, + targetZ: 4.7, +}; + +function drawScene(projectionMatrix, cameraMatrix) { + // Получаем view-матрицу из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + + let textureWorldMatrix = m4.lookAt( + [settings.posX, settings.posY, settings.posZ], // позиция + [settings.targetX, settings.targetY, settings.targetZ], // цель + [0, 1, 0], // up + ); + textureWorldMatrix = m4.scale( + textureWorldMatrix, + settings.projWidth, settings.projHeight, 1, + ); + + // используем обратную этой мировой матрицы, чтобы сделать + // матрицу, которая будет преобразовывать другие позиции + // относительно этого мирового пространства. + const textureMatrix = m4.inverse(textureWorldMatrix); + + // устанавливаем uniforms, которые одинаковы для сферы и плоскости + twgl.setUniforms(textureProgramInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, + u_textureMatrix: textureMatrix, + u_projectedTexture: imageTexture, + }); + + // ------ Рисуем сферу -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(sphereVAO); + + // Устанавливаем uniforms, уникальные для сферы + twgl.setUniforms(textureProgramInfo, sphereUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, sphereBufferInfo); + + // ------ Рисуем плоскость -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(planeVAO); + + // Устанавливаем uniforms, уникальные для плоскости + twgl.setUniforms(textureProgramInfo, planeUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, planeBufferInfo); + + // ------ Рисуем каркасный куб -------- + + gl.useProgram(colorProgramInfo.program); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(cubeLinesVAO); + + // Устанавливаем uniforms для каркасного куба + twgl.setUniforms(colorProgramInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, + u_world: textureWorldMatrix, + u_color: [1, 1, 1, 1], // белый + }); + + // Рисуем линии + gl.drawElements(gl.LINES, cubeLinesBufferInfo.numElements, gl.UNSIGNED_SHORT, 0); +} +``` + +Конечно, вам не обязательно использовать `lookAt`. Вы можете создать +мировую матрицу любым выбранным способом, например, используя +[граф сцены](webgl-scene-graph.html) или [стек матриц](webgl-2d-matrix-stack.html). + +Перед тем как запустить, давайте добавим какой-то масштаб. + +```js +const settings = { + cameraX: 2.75, + cameraY: 5, + posX: 3.5, + posY: 4.4, + posZ: 4.7, + targetX: 0.8, + targetY: 0, + targetZ: 4.7, + projWidth: 2, + projHeight: 2, +}; + +function drawScene(projectionMatrix, cameraMatrix) { + // Получаем view-матрицу из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + + let textureWorldMatrix = m4.lookAt( + [settings.posX, settings.posY, settings.posZ], // позиция + [settings.targetX, settings.targetY, settings.targetZ], // цель + [0, 1, 0], // up + ); + textureWorldMatrix = m4.scale( + textureWorldMatrix, + settings.projWidth, settings.projHeight, 1, + ); + + // используем обратную этой мировой матрицы, чтобы сделать + // матрицу, которая будет преобразовывать другие позиции + // относительно этого мирового пространства. + const textureMatrix = m4.inverse(textureWorldMatrix); + + ... +} +``` + +И с этим мы получаем спроецированную текстуру. + +{{{example url="../webgl-planar-projection.html"}}} + +Я думаю, может быть трудно увидеть пространство, в котором находится текстура. +Давайте добавим каркасный куб для визуализации. + +Сначала нам нужен отдельный набор шейдеров. Эти шейдеры +могут просто рисовать сплошной цвет, без текстур. + +```js +const colorVS = `#version 300 es +in vec4 a_position; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; + +void main() { + // Умножаем позицию на матрицы. + gl_Position = u_projection * u_view * u_world * a_position; +} +`; +``` + +```js +const colorFS = `#version 300 es +precision highp float; + +uniform vec4 u_color; + +out vec4 outColor; + +void main() { + outColor = u_color; +} +`; +``` + +Затем нам нужно скомпилировать и связать эти шейдеры тоже. + +```js +// настройка GLSL программ +const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs]); +const colorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS]); +``` + +И нам нужны данные для рисования куба из линий. + +```js +const sphereBufferInfo = primitives.createSphereBufferInfo( + gl, + 1, // радиус + 12, // делений по кругу + 6, // делений по высоте +); +const sphereVAO = twgl.createVAOFromBufferInfo( + gl, textureProgramInfo, sphereBufferInfo); +const planeBufferInfo = primitives.createPlaneBufferInfo( + gl, + 20, // ширина + 20, // высота + 1, // делений по ширине + 1, // делений по высоте +); +const planeVAO = twgl.createVAOFromBufferInfo( + gl, textureProgramInfo, planeBufferInfo); +``` + +Теперь давайте создадим данные для каркасного куба. + +```js +// Создаём данные для каркасного куба +const cubeLinesBufferInfo = twgl.createBufferInfoFromArrays(gl, { + position: { + numComponents: 3, + data: [ + // передняя грань + -1, -1, 1, + 1, -1, 1, + 1, 1, 1, + -1, 1, 1, + // задняя грань + -1, -1, -1, + 1, -1, -1, + 1, 1, -1, + -1, 1, -1, + ], + }, + indices: { + numComponents: 2, + data: [ + // передняя грань + 0, 1, 1, 2, 2, 3, 3, 0, + // задняя грань + 4, 5, 5, 6, 6, 7, 7, 4, + // соединяющие линии + 0, 4, 1, 5, 2, 6, 3, 7, + ], + }, +}); +const cubeLinesVAO = twgl.createVAOFromBufferInfo( + gl, colorProgramInfo, cubeLinesBufferInfo); +``` + +Теперь давайте добавим код для рисования каркасного куба в нашу функцию `drawScene`. + +```js +function drawScene(projectionMatrix, cameraMatrix) { + // Получаем view-матрицу из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + + let textureWorldMatrix = m4.lookAt( + [settings.posX, settings.posY, settings.posZ], // позиция + [settings.targetX, settings.targetY, settings.targetZ], // цель + [0, 1, 0], // up + ); + textureWorldMatrix = m4.scale( + textureWorldMatrix, + settings.projWidth, settings.projHeight, 1, + ); + + // используем обратную этой мировой матрицы, чтобы сделать + // матрицу, которая будет преобразовывать другие позиции + // относительно этого мирового пространства. + const textureMatrix = m4.inverse(textureWorldMatrix); + + // устанавливаем uniforms, которые одинаковы для сферы и плоскости + twgl.setUniforms(textureProgramInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, + u_textureMatrix: textureMatrix, + u_projectedTexture: imageTexture, + }); + + // ------ Рисуем сферу -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(sphereVAO); + + // Устанавливаем uniforms, уникальные для сферы + twgl.setUniforms(textureProgramInfo, sphereUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, sphereBufferInfo); + + // ------ Рисуем плоскость -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(planeVAO); + + // Устанавливаем uniforms, уникальные для плоскости + twgl.setUniforms(textureProgramInfo, planeUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, planeBufferInfo); + + // ------ Рисуем каркасный куб -------- + + gl.useProgram(colorProgramInfo.program); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(cubeLinesVAO); + + // Устанавливаем uniforms для каркасного куба + twgl.setUniforms(colorProgramInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, + u_world: textureWorldMatrix, + u_color: [1, 1, 1, 1], // белый + }); + + // Рисуем линии + gl.drawElements(gl.LINES, cubeLinesBufferInfo.numElements, gl.UNSIGNED_SHORT, 0); +} +``` + +И с этим мы получаем каркасный куб, который показывает пространство проекции. + +{{{example url="../webgl-planar-projection-with-lines.html"}}} + +Теперь вы можете видеть каркасный куб, который показывает, где проецируется текстура. +Вы можете изменить настройки, чтобы переместить проектор и изменить его ориентацию. + +Это планарное проекционное отображение. Текстура проецируется как плоскость. +Если вы хотите перспективное проекционное отображение, где текстура увеличивается +с расстоянием, вам нужно будет изменить матрицу проекции. + +Для перспективного проекционного отображения вы можете использовать +матрицу перспективы вместо ортографической матрицы в `u_textureMatrix`. +Это создаст эффект, где текстура увеличивается с расстоянием от проектора, +точно так же, как настоящий кинопроектор. + +Проекционное отображение — это мощная техника для создания +реалистичных эффектов, таких как проецирование изображений на стены, +создание голографических эффектов или добавление динамического освещения +к статическим объектам. diff --git a/webgl/lessons/ru/webgl-points-lines-triangles.md b/webgl/lessons/ru/webgl-points-lines-triangles.md index fc0962c4c..c93c2800c 100644 --- a/webgl/lessons/ru/webgl-points-lines-triangles.md +++ b/webgl/lessons/ru/webgl-points-lines-triangles.md @@ -128,4 +128,6 @@ WebGL рисует точки, линии и треугольники. Он де показать контуры многоугольников в программе 3D моделирования, использование `LINES` может быть отличным, но если вы хотите нарисовать структурированную графику, такую как SVG или Adobe Illustrator, то это не будет работать, и вам придется -[рендерить ваши линии каким-то другим способом, обычно из треугольников](https://mattdesl.svbtle.com/drawing-lines-is-hard). \ No newline at end of file +[рендерить ваши линии каким-то другим способом, обычно из треугольников](https://mattdesl.svbtle.com/drawing-lines-is-hard). + + \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-precision-issues.md b/webgl/lessons/ru/webgl-precision-issues.md index d7e35546d..bdd32621b 100644 --- a/webgl/lessons/ru/webgl-precision-issues.md +++ b/webgl/lessons/ru/webgl-precision-issues.md @@ -254,18 +254,18 @@ gl.texImage2D(
-Одна вещь для заметки в том, что по умолчанию WebGL может дизерить свои результаты, чтобы сделать -градации, как эта, выглядеть более гладкими. Вы можете выключить дизеринг с +Одна вещь, которую стоит отметить, это то, что по умолчанию WebGL может дизерить свои результаты, чтобы сделать +такие градации более гладкими. Вы можете отключить дизеринг с помощью ```js gl.disable(gl.DITHER); ``` -Если я не выключаю дизеринг, то мой смартфон производит это. +Если я не отключаю дизеринг, то мой смартфон производит это.
Сходу единственное место, где это действительно возникло бы, это если бы вы -использовали некоторый формат текстуры с более низким битовым разрешением как цель рендеринга и не +использовали какую-то текстуру с более низким битовым разрешением как цель рендеринга и не тестировали на устройстве, где эта текстура действительно имеет это более низкое разрешение. -Если вы только тестировали на настольном компьютере, любые проблемы, которые это вызывает, могут не быть очевидными. \ No newline at end of file +Если вы тестировали только на настольном компьютере, любые проблемы, которые это вызывает, могут быть не очевидны. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-pulling-vertices.md b/webgl/lessons/ru/webgl-pulling-vertices.md index 236166edb..fd2d62f93 100644 --- a/webgl/lessons/ru/webgl-pulling-vertices.md +++ b/webgl/lessons/ru/webgl-pulling-vertices.md @@ -195,4 +195,256 @@ gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(positionIndexUVIndex), gl.STATIC_ // Включаем атрибут индекса позиции gl.enableVertexAttribArray(posTexIndexLoc); -// Говорим атрибуту индекса позиции/texcoord, как забирать данные из буфера \ No newline at end of file +// Говорим атрибуту индекса позиции/texcoord, как забирать данные из буфера +// positionIndexUVIndexBuffer (ARRAY_BUFFER) +{ + const size = 2; // 2 компонента на итерацию + const type = gl.INT; // данные - 32-битные целые числа + const stride = 0; // 0 = двигаться вперёд на size * sizeof(type) каждый раз для получения следующей позиции + const offset = 0; // начинать с начала буфера + gl.vertexAttribIPointer( + posTexIndexLoc, size, type, stride, offset); +} +``` + +Обратите внимание, что мы вызываем `gl.vertexAttribIPointer`, а не `gl.vertexAttribPointer`. +`I` означает integer и используется для целочисленных и беззнаковых целочисленных атрибутов. +Также заметьте, что size равен 2, поскольку на вершину приходится 1 индекс позиции и 1 индекс texcoord. + +Хотя нам нужно только 24 вершины, мы всё равно должны рисовать 6 граней, 12 треугольников +каждая, 3 вершины на треугольник = 36 вершин. Чтобы указать, какие 6 вершин +использовать для каждой грани, мы будем использовать [индексы вершин](webgl-indexed-vertices.html). + +```js +const indices = [ + 0, 1, 2, 2, 1, 3, // front + 4, 5, 6, 6, 5, 7, // right + 8, 9, 10, 10, 9, 11, // back + 12, 13, 14, 14, 13, 15, // left + 16, 17, 18, 18, 17, 19, // top + 20, 21, 22, 22, 21, 23, // bottom +]; +// Создаём индексный буфер +const indexBuffer = gl.createBuffer(); +gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); +// Кладём индексы в буфер +gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); +``` + +Поскольку мы хотим нарисовать изображение на самом кубе, нам нужна 3-я текстура +с этим изображением. Давайте просто создадим ещё одну 4x4 data-текстуру с шахматной доской. +Мы будем использовать `gl.LUMINANCE` как формат, поскольку тогда нам нужен только один байт на пиксель. + +```js +// Создаём текстуру-шахматку +const checkerTexture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, checkerTexture); +// Заполняем текстуру 4x4 серой шахматкой +gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.LUMINANCE, + 4, + 4, + 0, + gl.LUMINANCE, + gl.UNSIGNED_BYTE, + new Uint8Array([ + 0xDD, 0x99, 0xDD, 0xAA, + 0x88, 0xCC, 0x88, 0xDD, + 0xCC, 0x88, 0xCC, 0xAA, + 0x88, 0xCC, 0x88, 0xCC, + ]), +); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +``` + +Переходим к вершинному шейдеру... Мы можем получить пиксель из текстуры так: + +```glsl +vec4 color = texelFetch(sampler2D tex, ivec2 pixelCoord, int mipLevel); +``` + +Итак, по целочисленным координатам пикселя код выше извлечёт значение пикселя. + +Используя функцию `texelFetch`, мы можем взять 1D индекс массива +и найти значение в 2D текстуре так: + +```glsl +vec4 getValueByIndexFromTexture(sampler2D tex, int index) { + int texWidth = textureSize(tex, 0).x; + int col = index % texWidth; + int row = index / texWidth; + return texelFetch(tex, ivec2(col, row), 0); +} +``` + +Итак, учитывая эту функцию, вот наш шейдер: + +```glsl +#version 300 es +in ivec2 positionAndTexcoordIndices; + +uniform sampler2D positionTexture; +uniform sampler2D texcoordTexture; + +uniform mat4 u_matrix; + +out vec2 v_texcoord; + +vec4 getValueByIndexFromTexture(sampler2D tex, int index) { + int texWidth = textureSize(tex, 0).x; + int col = index % texWidth; + int row = index / texWidth; + return texelFetch(tex, ivec2(col, row), 0); +} + +void main() { + int positionIndex = positionAndTexcoordIndices.x; + vec3 position = getValueByIndexFromTexture( + positionTexture, positionIndex).xyz; + + // Умножаем позицию на матрицу + gl_Position = u_matrix * vec4(position, 1); + + int texcoordIndex = positionAndTexcoordIndices.y; + vec2 texcoord = getValueByIndexFromTexture( + texcoordTexture, texcoordIndex).xy; + + // Передаём texcoord в фрагментный шейдер + v_texcoord = texcoord; +} +``` + +Внизу это по сути тот же шейдер, который мы использовали +в [статье о текстурах](webgl-3d-textures.html). +Мы умножаем `position` на `u_matrix` и выводим +texcoord в `v_texcoord` для передачи в фрагментный шейдер. + +Разница только в том, как мы получаем position и +texcoord. Мы используем переданные индексы и получаем +эти значения из соответствующих текстур. + +Чтобы использовать шейдер, нужно найти все локации: + +```js +// настраиваем GLSL программу +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); + +// ищем, куда должны идти вершинные данные +const posTexIndexLoc = gl.getAttribLocation( + program, "positionAndTexcoordIndices"); + +// ищем uniform'ы +const matrixLoc = gl.getUniformLocation(program, "u_matrix"); +const positionTexLoc = gl.getUniformLocation(program, "positionTexture"); +const texcoordTexLoc = gl.getUniformLocation(program, "texcoordTexture"); +const u_textureLoc = gl.getUniformLocation(program, "u_texture"); +``` + +Во время рендеринга настраиваем атрибуты: + +```js +// Говорим использовать нашу программу (пару шейдеров) +gl.useProgram(program); + +// Устанавливаем буфер и состояние атрибутов +gl.bindVertexArray(vao); +``` + +Затем нужно привязать все 3 текстуры и настроить все +uniform'ы: + +```js +// Устанавливаем матрицу +gl.uniformMatrix4fv(matrixLoc, false, matrix); + +// кладём текстуру позиций на texture unit 0 +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, positionTexture); +// Говорим шейдеру использовать texture unit 0 для positionTexture +gl.uniform1i(positionTexLoc, 0); + +// кладём текстуру texcoord на texture unit 1 +gl.activeTexture(gl.TEXTURE0 + 1); +gl.bindTexture(gl.TEXTURE_2D, texcoordTexture); +// Говорим шейдеру использовать texture unit 1 для texcoordTexture +gl.uniform1i(texcoordTexLoc, 1); + +// кладём текстуру-шахматку на texture unit 2 +gl.activeTexture(gl.TEXTURE0 + 2); +gl.bindTexture(gl.TEXTURE_2D, checkerTexture); +// Говорим шейдеру использовать texture unit 2 для u_texture +gl.uniform1i(u_textureLoc, 2); +``` + +И наконец рисуем: + +```js +// Рисуем геометрию +gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0); +``` + +И получаем куб с текстурой, используя только 8 позиций и +4 текстурные координаты: + +{{{example url="../webgl-pulling-vertices.html"}}} + +Несколько вещей для заметки. Код ленивый и использует 1D +текстуры для позиций и текстурных координат. +Текстуры могут быть только такой ширины. [Насколько широкими - зависит от машины](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE), что можно запросить с помощью: + +```js +const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); +``` + +Если бы мы хотели обработать больше данных, чем это, нам нужно было бы +выбрать какой-то размер текстуры, который подходит нашим данным, и распределить +данные по нескольким строкам, возможно +дополняя последнюю строку, чтобы получился прямоугольник. + +Ещё одна вещь, которую мы делаем здесь - используем 2 текстуры, +одну для позиций, одну для текстурных координат. +Нет причин, по которым мы не могли бы положить оба данных в +ту же текстуру либо чередуя: + + pos,uv,pos,uv,pos,uv... + +либо в разных местах в текстуре: + + pos,pos,pos,... + uv, uv, uv,... + +Нам просто пришлось бы изменить математику в вершинном шейдере, +которая вычисляет, как их извлекать из текстуры. + +Возникает вопрос: стоит ли делать такие вещи? +Ответ: "зависит от обстоятельств". В зависимости от GPU это +может быть медленнее, чем более традиционный способ. + +Цель этой статьи была в том, чтобы ещё раз указать, +что WebGL не заботится о том, как вы устанавливаете `gl_Position` с +координатами clip space, и не заботится о том, как вы +выводите цвет. Ему важно только, чтобы вы их установили. +Текстуры - это действительно просто 2D массивы данных с произвольным доступом. + +Когда у вас есть проблема, которую вы хотите решить в WebGL, +помните, что WebGL просто запускает шейдеры, и эти шейдеры +имеют доступ к данным через uniform'ы (глобальные переменные), +атрибуты (данные, которые приходят за итерацию вершинного шейдера), +и текстуры (2D массивы с произвольным доступом). Не позволяйте +традиционным способам использования WebGL помешать вам +увидеть настоящую гибкость, которая там есть. + +
+

Почему это называется Vertex Pulling?

+

Я на самом деле слышал этот термин только недавно (июль 2019), +хотя использовал технику раньше. Он происходит из +статьи OpenGL Insights "Programmable Vertex Pulling" от Daniel Rakos. +

+

Это называется vertex *pulling* (вытягивание вершин), поскольку это вершинный шейдер +решает, какие вершинные данные читать, в отличие от традиционного способа, где +вершинные данные поставляются автоматически через атрибуты. По сути +вершинный шейдер *вытягивает* данные из памяти.

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md b/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md deleted file mode 100644 index 5909ea41a..000000000 --- a/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md +++ /dev/null @@ -1,46 +0,0 @@ -Title: Доступ к текстурам по координатам пикселей в WebGL2 -Description: Доступ к текстурам по координатам пикселей в WebGL2 -TOC: Доступ к текстурам по координатам пикселей в WebGL2 - -## Вопрос: - -Из https://webgl2fundamentals.org/webgl/lessons/webgl-image-processing.html - -> WebGL2 добавляет возможность читать текстуру, используя координаты пикселей. Какой способ лучше - решать вам. Я считаю, что более распространено использование координат текстуры, чем координат пикселей. - -Нигде больше это не упоминается, кроме как передачи uniform с размерами текстуры в пикселях и вычисления оттуда, есть ли способ получить доступ к этим координатам пикселей без вычислений, как предполагается здесь? - - - -## Ответ: - -Вы можете читать отдельные пиксели/тексели из текстуры в WebGL2 с помощью `texelFetch`: - - vec4 color = texelFetch(someUniformSampler, ivec2(pixelX, pixelY), intMipLevel); - -Например, вычислить средний цвет текстуры, читая каждый пиксель: - -{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-1.html"}}} - -примечания: поскольку холст RGBA8 может получить только целочисленный результат. Можно изменить на какой-то формат с плавающей точкой, но это усложнит пример, который не о рендеринге, а о `texelFetch`. - -Конечно, просто изменив данные с R8 на RGBA8, мы можем делать 4 массива одновременно, если мы чередуем значения: - -{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-2.html"}}} - -Чтобы сделать больше, нужно придумать какой-то способ организовать данные и использовать вход в фрагментный шейдер, чтобы понять, где находятся данные. Например, мы снова чередуем данные, 5 массивов, так что данные идут 0,1,2,3,4,0,1,2,3,4,0,1,2,3,4. - -Давайте вернемся к R8 и сделаем 5 отдельных массивов. Нам нужно нарисовать 5 пикселей. Мы можем сказать, какой пиксель рисуется, посмотрев на `gl_FragCoord`. Мы можем использовать это для смещения, какие пиксели мы смотрим, и передать, сколько пропустить. - -{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-3.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - bogersja - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md b/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md deleted file mode 100644 index d9a40039a..000000000 --- a/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md +++ /dev/null @@ -1,326 +0,0 @@ -Title: Применение карты смещения и карты бликов -Description: Применение карты смещения и карты бликов -TOC: Применение карты смещения и карты бликов - -## Вопрос: - -Я пытаюсь применить как карту смещения, так и карту бликов для Земли, и только карту смещения для Луны. - -Я мог бы преобразовать карту высот в карту нормалей, но если я использую ту же карту высот для применения карты смещения, это не работает так, как я ожидал. - -Вот пример изображения: - -[![Пример 1][1]][1] - -как вы можете видеть, неровности вокруг Земли и Луны, но нет реальных различий в высоте. - -Если я применяю карту бликов к Земле, Земля становится такой: - -[![Пример 2][2]][2] - -Я хочу, чтобы только океан Земли блестел, но мой код превращает Землю в полностью черную, я вижу только некоторые белые точки на Земле... - -Эти текстуры взяты с этого [сайта][3] - -Вот мой код вершинного шейдера и фрагментного шейдера: - - "use strict"; - const loc_aPosition = 3; - const loc_aNormal = 5; - const loc_aTexture = 7; - const VSHADER_SOURCE = - `#version 300 es - layout(location=${loc_aPosition}) in vec4 aPosition; - layout(location=${loc_aNormal}) in vec4 aNormal; - layout(location=${loc_aTexture}) in vec2 aTexCoord; - - - uniform mat4 uMvpMatrix; - uniform mat4 uModelMatrix; // Матрица модели - uniform mat4 uNormalMatrix; // Матрица преобразования нормали - - uniform sampler2D earth_disp; - uniform sampler2D moon_disp; - - //uniform float earth_dispScale; - //uniform float moon_dispScale; - - //uniform float earth_dispBias; - //uniform float moon_dispBias; - - uniform bool uEarth; - uniform bool uMoon; - - - out vec2 vTexCoord; - out vec3 vNormal; - out vec3 vPosition; - - - void main() - { - - float disp; - - if(uEarth) - disp = texture(earth_disp, aTexCoord).r; //Извлечение цветовой информации из изображения - else if(uMoon) - disp = texture(moon_disp, aTexCoord).r; //Извлечение цветовой информации из изображения - - vec4 displace = aPosition; - - float displaceFactor = 2.0; - float displaceBias = 0.5; - - if(uEarth || uMoon) //Использование карты смещения - { - displace += (displaceFactor * disp - displaceBias) * aNormal; - gl_Position = uMvpMatrix * displace; - } - else //Не используем карту смещения - gl_Position = uMvpMatrix * aPosition; - - // Вычисляем позицию вершины в мировой системе координат - vPosition = vec3(uModelMatrix * aPosition); - - vNormal = normalize(vec3(uNormalMatrix * aNormal)); - vTexCoord = aTexCoord; - - }`; - - // Программа фрагментного шейдера - const FSHADER_SOURCE = - `#version 300 es - precision mediump float; - - uniform vec3 uLightColor; // Цвет света - uniform vec3 uLightPosition; // Позиция источника света - uniform vec3 uAmbientLight; // Цвет окружающего света - - uniform sampler2D sun_color; - uniform sampler2D earth_color; - uniform sampler2D moon_color; - - uniform sampler2D earth_bump; - uniform sampler2D moon_bump; - - uniform sampler2D specularMap; - - - in vec3 vNormal; - in vec3 vPosition; - in vec2 vTexCoord; - out vec4 fColor; - - uniform bool uIsSun; - uniform bool uIsEarth; - uniform bool uIsMoon; - - - - vec2 dHdxy_fwd(sampler2D bumpMap, vec2 UV, float bumpScale) - { - vec2 dSTdx = dFdx( UV ); - vec2 dSTdy = dFdy( UV ); - float Hll = bumpScale * texture( bumpMap, UV ).x; - float dBx = bumpScale * texture( bumpMap, UV + dSTdx ).x - Hll; - float dBy = bumpScale * texture( bumpMap, UV + dSTdy ).x - Hll; - return vec2( dBx, dBy ); - } - - vec3 pertubNormalArb(vec3 surf_pos, vec3 surf_norm, vec2 dHdxy) - { - vec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) ); - vec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) ); - vec3 vN = surf_norm; // нормализованная - vec3 R1 = cross( vSigmaY, vN ); - vec3 R2 = cross( vN, vSigmaX ); - float fDet = dot( vSigmaX, R1 ); - fDet *= ( float( gl_FrontFacing ) * 2.0 - 1.0 ); - vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 ); - return normalize( abs( fDet ) * surf_norm - vGrad ); - } - - - - void main() - { - vec2 dHdxy; - vec3 bumpNormal; - float bumpness = 1.0; - if(uIsSun) - fColor = texture(sun_color, vTexCoord); - else if(uIsEarth) - { - fColor = texture(earth_color, vTexCoord); - dHdxy = dHdxy_fwd(earth_bump, vTexCoord, bumpness); - } - else if(uIsMoon) - { - fColor = texture(moon_color, vTexCoord); - dHdxy = dHdxy_fwd(moon_bump, vTexCoord, bumpness); - } - - - - // Нормализуем нормаль, потому что она интерполируется и больше не имеет длину 1.0 - vec3 normal = normalize(vNormal); - - - // Вычисляем направление света и делаем его длину равной 1. - vec3 lightDirection = normalize(uLightPosition - vPosition); - - - - // Скалярное произведение направления света и ориентации поверхности (нормали) - float nDotL; - if(uIsSun) - nDotL = 1.0; - else - nDotL = max(dot(lightDirection, normal), 0.0); - - - - // Вычисляем финальный цвет из диффузного отражения и окружающего отражения - vec3 diffuse = uLightColor * fColor.rgb * nDotL; - vec3 ambient = uAmbientLight * fColor.rgb; - float specularFactor = texture(specularMap, vTexCoord).r; //Извлечение цветовой информации из изображения - - - - - vec3 diffuseBump; - if(uIsEarth || uIsMoon) - { - bumpNormal = pertubNormalArb(vPosition, normal, dHdxy); - diffuseBump = min(diffuse + dot(bumpNormal, lightDirection), 1.1); - } - - vec3 specular = vec3(0.0); - float shiness = 12.0; - vec3 lightSpecular = vec3(1.0); - - if(uIsEarth && nDotL > 0.0) - { - vec3 v = normalize(-vPosition); // Позиция глаза - vec3 r = reflect(-lightDirection, bumpNormal); // Отражение от поверхности - specular = lightSpecular * specularFactor * pow(dot(r, v), shiness); - } - - //Обновляем финальный цвет - if(uIsEarth) - fColor = vec4( (diffuse * diffuseBump * specular) + ambient, fColor.a); // Блики - else if(uIsMoon) - fColor = vec4( (diffuse * diffuseBump) + ambient, fColor.a); - else if(uIsSun) - fColor = vec4(diffuse + ambient, fColor.a); - }`; - - -Можете ли вы сказать мне, где мне нужно проверить? - - [1]: https://i.stack.imgur.com/eJgLg.png - [2]: https://i.stack.imgur.com/zRSZu.png - [3]: http://planetpixelemporium.com/earth.html - -## Ответ: - -Если бы это был я, я бы сначала упростил шейдер до самой простой вещи и посмотрел, получаю ли я то, что хочу. Вы хотите блики, так получаете ли вы блики только с расчетами бликов в ваших шейдерах? - -Обрезка ваших шейдеров до простого рисования плоского освещения по Фонгу не дала правильных результатов. - -Эта строка: - -``` -fColor = vec4( (diffuse * specular) + ambient, fColor.a); -``` - -должна была быть: - -``` -fColor = vec4( (diffuse + specular) + ambient, fColor.a); -``` - -Вы добавляете блики, а не умножаете на них. - -{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-1.html"}}} - -Теперь мы можем добавить карту бликов: - -{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-2.html"}}} - -Затем вам, возможно, не стоит использовать много булевых условий в вашем шейдере. Либо создайте разные шейдеры, либо найдите способ сделать это без булевых значений. Так, например, нам не нужны: - -``` -uniform sampler2D earth_disp; -uniform sampler2D moon_disp; - -uniform sampler2D sun_color; -uniform sampler2D earth_color; -uniform sampler2D moon_color; - -uniform sampler2D earth_bump; -uniform sampler2D moon_bump; - -uniform bool uIsSun; -uniform bool uIsEarth; -uniform bool uIsMoon; -``` - -мы можем просто иметь: - -``` -uniform sampler2D displacementMap; -uniform sampler2D surfaceColor; -uniform sampler2D bumpMap; -``` - -Затем мы можем установить `displacementMap` и `bumpMap` на текстуру одного пикселя 0,0,0,0, и не будет ни смещения, ни неровностей. - -Что касается разного освещения для солнца, учитывая, что солнце не использует ни карту неровностей, ни карту смещения, ни даже освещение вообще, возможно, было бы лучше использовать другой шейдер, но мы также можем просто добавить значение `maxDot` так: - -``` -uniform float maxDot; - -... - - nDotL = max(dot(lightDirection, normal), maxDot) -``` - -Если `maxDot` равен нулю, мы получим нормальное скалярное произведение. Если `maxDot` равен единице, мы не получим освещения. - -{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-3.html"}}} - -Что касается смещения, смещение работает только на вершинах, поэтому вам нужно много вершин в вашей сфере, чтобы увидеть любое смещение. - -Также была ошибка, связанная со смещением. Вы передаете нормали как vec4, и эта строка: - - displace += (displaceFactor * disp - displaceBias) * aNormal; - -В итоге добавляет смещение vec4. Другими словами, допустим, вы начали с `a_Position` равным `vec4(1,0,0,1)`, что было бы на левой стороне сферы. `aNormal`, потому что вы объявили его как `vec4`, вероятно, тоже `vec4(1,0,0,1)`. Предполагая, что вы фактически передаете данные vec3 нормали через атрибуты из вашего буфера, значение по умолчанию для W равно 1. Допустим, `disp` равен 1, `displaceFactor` равен 2, а `displaceBias` равен 0.5, что у вас было. Вы получаете: - - displace = vec4(1,0,0,1) + (2 * 1 + 0.5) * vec4(1,0,0,1) - displace = vec4(1,0,0,1) + (1.5) * vec4(1,0,0,1) - displace = vec4(1,0,0,1) + vec4(1.5,0,0,1.5) - displace = vec4(2.5,0,0,2.5) - -Но вы не хотите, чтобы W был 2.5. Одно исправление - просто использовать xyz часть нормали: - - displace.xyz += (displaceFactor * disp - displaceBias) * aNormal.xyz; - -Более нормальное исправление - объявить атрибут нормали только как vec3: - - in vec3 aNormal; - - displace.xyz += (displaceFactor * disp - displaceBias) * aNormal; - -В моем примере выше сферы имеют только радиус = 1, поэтому мы хотим только немного скорректировать это смещение. Я установил `displaceFactor` равным 0.1 и `displaceBias` равным 0. - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - ZeroFive005 - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md b/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md deleted file mode 100644 index 804af4c88..000000000 --- a/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md +++ /dev/null @@ -1,154 +0,0 @@ -Title: Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер? -Description: Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер? -TOC: Can anyone explain what this GLSL fragment shader is doing? - -## Вопрос: - -Я понимаю, что это вопрос, ориентированный на математику, но... если вы посмотрите на [эту веб-страницу](https://threejs.org/examples/?q=shader#webgl_shader). (и у вас есть хорошая видеокарта) - -Если вы посмотрите на исходный код, вы заметите страшно выглядящий фрагментный шейдер. - -Я не ищу подробного объяснения, но идею о том, что происходит, или источник информации о том, что именно происходит здесь.. Я не ищу руководство по GLSL, но информацию о математике. Я понимаю, что это может быть лучше подходит для сайта Math StackExchange, но подумал, что попробую здесь сначала... - - - -## Ответ: - -[Monjori](http://www.pouet.net/prod.php?which=52761) из демо-сцены. - -Простой ответ - он использует формулу для генерации паттерна. WebGL будет вызывать эту функцию один раз для каждого пикселя на экране. Единственные вещи, которые будут меняться, это time и gl_FragCoord, который является местоположением пикселя, который рисуется. - -Давайте разберем это немного - - - // это разрешение окна - uniform vec2 resolution; - - // это счетчик в секундах. - uniform float time; - - void main() { - // gl_FragCoord - это позиция пикселя, который рисуется - // поэтому этот код делает p значением, которое идет от -1 до +1 - // x и y - vec2 p = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy; - - // a = время ускоренное в 40 раз - float a = time*40.0; - - // объявляем кучу переменных. - float d,e,f,g=1.0/40.0,h,i,r,q; - - // e идет от 0 до 400 по экрану - e=400.0*(p.x*0.5+0.5); - - // f идет от 0 до 400 вниз по экрану - f=400.0*(p.y*0.5+0.5); - - // i идет от 200 + или - 20 на основе - // sin от e * 1/40 + замедленное время / 150 - // или другими словами замедлить еще больше. - // e * 1/40 означает e идет от 0 до 1 - i=200.0+sin(e*g+a/150.0)*20.0; - - // d это 200 + или - 18.0 + или - 7 - // первый +/- это cos от 0.0 до 0.5 вниз по экрану - // второй +/- это cos от 0.0 до 1.0 по экрану - d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; - - // Я останавливаюсь здесь. Вы, вероятно, можете разобрать остальное - // смотрите ответ - r=sqrt(pow(i-e,2.0)+pow(d-f,2.0)); - q=f/r; - e=(r*cos(q))-a/2.0;f=(r*sin(q))-a/2.0; - d=sin(e*g)*176.0+sin(e*g)*164.0+r; - h=((f+d)+a/2.0)*g; - i=cos(h+r*p.x/1.3)*(e+e+a)+cos(q*g*6.0)*(r+h/3.0); - h=sin(f*g)*144.0-sin(e*g)*212.0*p.x; - h=(h+(f-e)*q+sin(r-(a+h)/7.0)*10.0+i/4.0)*g; - i+=cos(h*2.3*sin(a/350.0-q))*184.0*sin(q-(r*4.3+a/12.0)*g)+tan(r*g+h)*184.0*cos(r*g+h); - i=mod(i/5.6,256.0)/64.0; - if(i<0.0) i+=4.0; - if(i>=2.0) i=4.0-i; - d=r/350.0; - d+=sin(d*d*8.0)*0.52; - f=(sin(a*g)+1.0)/2.0; - gl_FragColor=vec4(vec3(f*i/1.6,i/2.0+d/13.0,i)*d*p.x+vec3(i/1.3+d/8.0,i/2.0+d/18.0,i)*d*(1.0-p.x),1.0); - } - -Одна из вещей, которую хорошо попробовать, чтобы увидеть, что происходит, это вставить ранние выходы в шейдер. Сначала вы можете увидеть шейдер здесь - -http://glsl.heroku.com/e#1579.0 - -или - -https://www.shadertoy.com/view/lsfyRS - -Если мы перейдем к строке 11 - - e=400.0*(p.x*0.5+0.5); - -и вставим сразу после нее что-то вроде этого - - e=400.0*(p.x*0.5+0.5); - gl_FragColor = vec4(e / 400.0, 0, 0, 1); - return; - -Пока мы конвертируем значение в что-то от 0 до 1, мы можем увидеть результат - -например, спускаясь к строке 14 - - d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; - -Поскольку мы знаем, что это идет от 200 +/- 18 +/- 7, это 175 + 225, поэтому конвертируем это в 0 до 1 с - - d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; - float tmp = (d - 175.0) / 50.0; - gl_FragColor = vec4(tmp, 0, 0, 1); - return; - -даст вам некоторое представление о том, что он делает. - -Я уверен, что вы можете разобрать остальное. - -
-
Вопрос и процитированные части являются - CC BY-SA 3.0 от - Alex - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md b/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md deleted file mode 100644 index aa70eafdc..000000000 --- a/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md +++ /dev/null @@ -1,73 +0,0 @@ -Title: Создание эффекта смазывания/жидкости (smudge/liquify) -Description: Создание эффекта смазывания/жидкости (smudge/liquify) -TOC: Создание эффекта смазывания/жидкости (smudge/liquify) - -## Вопрос: - -Я пытаюсь найти информацию или примеры, которые можно использовать для создания эффекта смазывания/жидкости, который постепенно анимируется обратно к исходному состоянию. - -Сначала я рассматривал вариант с использованием three.js или pixi.js для рендеринга текста, а затем с помощью событий мыши и ray casting вытягивать меш из позиции. Ближе всего, что я нашёл — это: - -https://codepen.io/shshaw/pen/qqVgbg - - let renderer = PIXI.autoDetectRenderer(window.innerWidth, - window.innerHeight, { transparent: true }); - -Думаю, в идеале я бы рендерил текст как изображение, а затем применял бы эффект смазывания к пикселям, которые медленно возвращались бы к исходному состоянию. Похоже на это: - -http://www.duhaihang.com/#/work/ - -Думаю, мне нужен кастомный GLSL-шейдер и какой-то буфер для хранения исходного и текущего состояния пикселей изображения. - -Любая помощь или направление будет очень полезно. - -## Ответ: - -Оба варианта довольно прямолинейны. - -Первый, как вы и сказали, — делаете меш (сетку) вершин, рисующую плоскость. Накладываете текстуру на плоскость, при перемещении мыши добавляете смещение каждой вершине, которую касается мышь. Со временем сбрасываете смещение обратно к 0 (т.е. 0 смещения). - -Вот пример: здесь смещается только одна вершина на случайную величину, а не что-то более предсказуемое. В конце я просто сохраняю время, к которому смещение должно исчезнуть, затем в шейдере делаю простой линейный lerp (можно сделать более сложный для эффекта bounce и т.д.). Так почти всё происходит в шейдере. - -{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-1.html"}}} - -Во втором варианте вместо смещения вершин делаете текстуру смещений, со временем сбрасываете это смещение к 0. - -Пример затухания можно посмотреть [здесь](https://stackoverflow.com/a/38407507/128511). Если взять этот пример и вместо рисования случайного квадрата рисовать под мышью, а затем использовать эту текстуру как смещение для основного изображения. Под смещением я имею в виду, что обычно вы берёте текстуру во фрагментном шейдере так: - - vec4 color = texture2D(someTexture, someTextureCoords); - -Вместо этого вы хотите сместить координаты текстуры с помощью displacement, примерно так: - - // если текстура смещений того же размера, что и основная, можно использовать те же координаты - - // сначала получаем смещение и переводим в диапазон -1 <-> 1 - // используем только R и G каналы, которые станут U и V смещениями для координат текстуры - vec2 displacement = texture2D(displacementTexture, someTextureCoords).rg * 2. - 1.; - - vec2 uv = someTextureCoords + displacement * displacementRange; - vec4 color = texture2d(someTexture, uv); - -Вот пример выше, используемый для смещения: - -{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-2.html"}}} - -Осталось только рисовать под мышью, а не в случайном месте: - -{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-3.html"}}} - -Точный эффект из вашего второго примера, похоже, использует какое-то шумовое преобразование displacement. Можно воспользоваться [WebGL Inspector](https://benvanik.github.io/WebGL-Inspector/) или [Shader Editor](https://github.com/spite/ShaderEditorExtension), чтобы посмотреть, что происходит в шейдерах. - -[Вот ещё пример](https://codepen.io/greggman/pen/bgXgvr), который создаёт текстуру смещений, смещающую больше к центру, чем к краю. - -Пояснение: я не смотрел детали того, как работают ваши примеры, я лишь предполагаю, что они делают что-то *похожее*. Лучший способ узнать, что они реально делают — посмотреть их код и воспользоваться инструментами выше, чтобы заглянуть внутрь. Может быть, они используют не прямое смещение, а, например, нормали как displacement. Может быть, вместо рисования сплошным цветом (2-й и 3-й примеры) или текстурой (4-й пример), они используют процедурный паттерн или экранные координаты для повторяющейся текстуры. Может быть, текстура смещений — отдельная текстура, а есть ещё "mix mask", которую рисуют белым и затухают в чёрный, чтобы определить, сколько displacement применять. В WebGL бесконечно много способов реализовать подобные эффекты. - - - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - plexus - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md b/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md deleted file mode 100644 index ce95d9d26..000000000 --- a/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md +++ /dev/null @@ -1,38 +0,0 @@ -Title: FPS-подобное движение камеры с базовыми матричными трансформациями -Description: FPS-подобное движение камеры с базовыми матричными трансформациями -TOC: FPS-подобное движение камеры с базовыми матричными трансформациями - -## Вопрос: - -У меня простая сцена в WebGL, где я храню каждую трансформацию (для камеры и моделей) в одной модели/видовой матрице и устанавливаю их, вращая и перемещая эту матрицу. - -Я хочу иметь возможность вращать камеру вокруг и когда я "двигаюсь вперёд", двигаться в направлении, куда указывает камера. - -Пока что я изменил [этот][1] код на это: - - mat4.identity(mvMatrix); - mat4.rotateX(mvMatrix, degToRad(elev), mvMatrix); - mat4.rotateY(mvMatrix, degToRad(ang), mvMatrix); - mat4.rotateZ(mvMatrix, degToRad(-roll), mvMatrix); - mat4.translate(mvMatrix, [-px, -py, -pz], mvMatrix); -поскольку это не работало как было, и это вроде работает, пока вы не делаете экстремальное вращение (больше 90 градусов). - -Это не критично для того, что я делаю, но я хочу знать. Это лучшее, что я могу получить, не отходя от расчёта ориентации камеры таким образом? - - [1]: https://stackoverflow.com/questions/18463868/webgl-translation-after-rotation-of-the-camera-as-an-fps - -## Ответ: - -WebGL камеры обычно указывают по оси -Z, так что чтобы двигаться в направлении, куда смотрит камера, вы просто добавляете Z-ось камеры (элементы 8, 9, 10) к позиции камеры, умноженной на некоторую скорость. - -{{{example url="../webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations-example-1.html"}}} - - - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - George Daskalakis - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md b/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md deleted file mode 100644 index e06cea7de..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md +++ /dev/null @@ -1,45 +0,0 @@ -Title: Как получить аудио данные в шейдер -Description: Как получить аудио данные в шейдер -TOC: Как получить аудио данные в шейдер - -## Вопрос: - -Как я могу добавить поддержку аудио визуализации к этому классу, я хотел бы добавить объект Audio() как вход в GLSL фрагментный шейдер. Пример этого https://www.shadertoy.com/view/Mlj3WV. Я знаю, что такие вещи можно делать в Canvas 2d с анализом формы волны, но этот метод opengl намного более плавный. - - -``` -/* Код из https://raw.githubusercontent.com/webciter/GLSLFragmentShader/1.0.0/GLSLFragmentShader.js */ - -/* функция рендеринга */ - -/* установить uniform переменные для шейдера */ - gl.uniform1f( currentProgram.uniformsCache[ 'time' ], parameters.time / 1000 ); -gl.uniform2f( currentProgram.uniformsCache[ 'mouse' ], parameters.mouseX, parameters.mouseY ); - -/* я хотел бы что-то вроде этого */ -gl.uniform2f( currentProgram.uniformsCache[ 'fft' ], waveformData ); - - -``` - -Шейдер в примере ShaderToy принимает float как fft, но это просто обновляет всю строку полосок, а не отдельные значения полосок. Я хотел бы манипуляции в реальном времени всех полосок. - -Я искал в MDN, но не понимаю, как это включить, я также смотрел исходный код shadertoy.com, но не могу понять, как они достигли этого. - -## Ответ: - -ShaderToy не предоставляет FFT как float. Он предоставляет данные FFT как текстуру - - -{{{example url="../webgl-qna-how-to-get-audio-data-into-a-shader-example-1.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - David Clews - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md b/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md deleted file mode 100644 index 200d862b9..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md +++ /dev/null @@ -1,83 +0,0 @@ -Title: Как получить автодополнение кода для WebGL в Visual Studio Code -Description: Как получить автодополнение кода для WebGL в Visual Studio Code -TOC: Как получить автодополнение кода для WebGL в Visual Studio Code - -## Вопрос: - -У меня есть школьный проект, и мне нужно использовать WEBGL. Но довольно сложно писать весь код без автодополнения. Я не нашел подходящего расширения. У вас есть идеи? - -## Ответ: - -Для того чтобы Visual Studio Code давал вам автодополнение, ему нужно знать типы переменных. - -Так, например, если у вас есть это - -``` -const gl = init(); -``` - -VSCode не имеет представления о том, какой тип у переменной `gl`, поэтому он не может автодополнять. Но вы можете сказать ему тип, добавив JSDOC стиль комментария выше, как это - -``` -/** @type {WebGLRenderingContext} */ -const gl = init(); -``` - -Теперь он будет автодополнять - -[![enter image description here][1]][1] - - -То же самое верно для HTML элементов. Если вы делаете это - -``` -const canvas = document.querySelector('#mycanvas'); -``` - -VSCode не имеет представления о том, какой это тип элемента, но вы можете сказать ему - -``` -/** @type {HTMLCanvasElement} */ -const canvas = document.querySelector('#mycanvas'); -``` - -Теперь он будет знать, что это `HTMLCanvasElement` - -[![enter image description here][2]][2] - -И, поскольку он знает, что это `HTMLCanvasElement`, он знает, что `.getContext('webgl')` возвращает `WebGLRenderingContext`, поэтому он автоматически предложит автодополнение для контекста тоже - -[![enter image description here][3]][3] - -Обратите внимание, что если вы передаете canvas в какую-то функцию, то снова VSCode не имеет представления о том, что возвращает эта функция. Другими словами - -``` -/** @type {HTMLCanvasElement} */ -const canvas = document.querySelector('#mycanvas'); -const gl = someLibraryInitWebGL(canvas); -``` - -Вы больше не получите автодополнение, поскольку VSCode не имеет представления о том, что возвращает `someLibraryInitWebGL`, поэтому следуйте правилу сверху и скажите ему. - -``` -/** @type {HTMLCanvasElement} */ -const canvas = document.querySelector('#mycanvas'); - -/** @type {WebGLRenderingContext} */ -const gl = someLibraryInitWebGL(canvas); -``` - -Вы можете увидеть другие JSDOC аннотации [здесь](https://jsdoc.app/), если хотите документировать свои собственные функции, например, их аргументы и типы возврата. - - [1]: https://i.stack.imgur.com/8mvFM.png - [2]: https://i.stack.imgur.com/oArWf.png - [3]: https://i.stack.imgur.com/7zR4q.png - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - Nikola Kovač - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md b/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md deleted file mode 100644 index 19663808e..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md +++ /dev/null @@ -1,55 +0,0 @@ -Title: Как получить эффект пикселизации в WebGL? -Description: Как получить эффект пикселизации в WebGL? -TOC: Как получить эффект пикселизации в WebGL? - -## Вопрос: - -Я хочу симулировать эффект старого ПК с низким разрешением, как Atari или Commodore, в WebGL. Есть ли способ нарисовать изображение, а затем как-то сделать пиксели больше? - -Я новичок в WebGL, так как мне начать делать этот эффект? - -Я нашёл [это](https://threejs.org/examples/#webgl_postprocessing_nodes), там есть эффект мозаики, но он использует three.js, а я хочу сделать это без фреймворков. - -## Ответ: - -Есть много способов сделать это. Самый простой — просто рендерить в низкоразрешающую текстуру, прикрепив её к framebuffer, а затем рендерить эту текстуру на canvas с фильтрацией текстуры, установленной на `NEAREST`. - -Вот пример. Он использует [TWGL](http://twgljs.org), который не является фреймворком, просто помощник, чтобы сделать WebGL менее многословным. Смотрите комментарии (и [документацию](http://twgljs.org/docs/)), если хотите перевести это в многословный сырой WebGL. - -Если вы новичок в WebGL, [я бы предложил начать отсюда](http://webglfundamentals.org): - -{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-1.html"}}} - -Также распространено рендерить в текстуру (как выше), но текстуру более высокого разрешения, а затем фильтровать её вниз, используя шейдеры, мипы и/или линейную фильтрацию. Преимущество в том, что вы получите больше сглаживания: - -{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-2.html"}}} - ---- - -# обновление - -В 2020 году, возможно, самое простое, что вы можете сделать — просто сделать canvas с разрешением, которое вы хотите, например 32x32, и установить его CSS размер больше, а затем использовать настройку CSS `image-rendering: pixelated`, чтобы сказать браузеру не сглаживать его при масштабировании изображения: - -``` - -``` - -{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-3.html"}}} - - - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - Maciej Kozieja - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md deleted file mode 100644 index 9f1b8e5b8..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md +++ /dev/null @@ -1,142 +0,0 @@ -Title: Как реализовать зум от мыши в 2D WebGL -Description: Как реализовать зум от мыши в 2D WebGL -TOC: Как реализовать зум от мыши в 2D WebGL - -## Вопрос: - -Я сейчас делаю 2D-рисовалку на WebGL. Хочу реализовать зум к точке под курсором мыши, как в [этом примере](https://stackoverflow.com/a/53193433/2594567). Но не могу понять, как применить решение из того ответа в своём случае. - -Я сделал базовый зум через масштабирование матрицы камеры. Но он увеличивает к левому верхнему углу canvas, потому что это начало координат (0,0), заданное проекцией (насколько я понимаю). - -Базовый пан и зум реализованы: -![img](https://i.imgur.com/asRTm1e.gif) - -### Моя функция draw (включая вычисления матриц) выглядит так: - -```javascript -var projection = null; -var view = null; -var viewProjection = null; - -function draw(gl, camera, sceneTree){ - // projection matrix - projection = new Float32Array(9); - mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight); - - // camera matrix - view = new Float32Array(9); - mat3.fromTranslation(view, camera.translation); - mat3.rotate(view, view, toRadians(camera.rotation)); - mat3.scale(view, view, camera.scale); - // view matrix - mat3.invert(view, view) - - // VP matrix - viewProjection = new Float32Array(9); - mat3.multiply(viewProjection, projection, view); - - // go through scene tree: - // - build final matrix for each object - // e.g: u_matrix = VP x Model (translate x rotate x scale) - - // draw each object in scene tree - // ... -} -``` - -### Вершинный шейдер: -``` -attribute vec2 a_position; - -uniform mat3 u_matrix; - -void main() { - gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); -} -``` - -### Функция зума: - -```javascript - -function screenToWorld(screenPos){ - // normalized screen position - let nsp = [ - 2.0 * screenPos[0] / this.gl.canvas.width - 1, - - 2.0 * screenPos[1] / this.gl.canvas.height + 1 - ]; - - let inverseVP = new Float32Array(9); - mat3.invert(inverseVP, viewProjection); - - let worldPos = [0, 0]; - return vec2.transformMat3(worldPos, nsp, inverseVP); -} - -var zoomRange = [0.01, 2]; - -canvas.addEventListener('wheel', (e) => { - let oldZoom = camera.scale[0]; - let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]); - - camera.scale = [zoom, zoom]; - - let zoomPoint = screenToWorld([e.clientX, e.clientY]); - // totally breaks if enable this line - //vec2.copy(camera.translation, zoomPoint); - - // call draw function again - draw(); - -}, false); -``` - - -Если я применяю `zoomPoint` к трансляции камеры, значения `zoomPoint` (и позиция камеры соответственно) начинают неуправляемо расти при каждом событии зума (неважно, увеличиваю я или уменьшаю), и объекты в сцене сразу выходят из вида. - -Буду очень благодарен за любые идеи или подсказки, что я делаю не так. Спасибо. - -## Ответ: - -Поскольку вы не выложили **минимальный воспроизводимый пример** в самом вопросе, я не мог проверить с вашей библиотекой матриц. Используя свою, я смог реализовать зум так: - -``` - const [clipX, clipY] = getClipSpaceMousePosition(e); - - // позиция до зума - const [preZoomX, preZoomY] = m3.transformPoint( - m3.inverse(viewProjectionMat), - [clipX, clipY]); - - // умножаем движение колеса на текущий уровень зума - // чтобы зумить меньше при большом увеличении и больше при малом - const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01); - camera.zoom = Math.max(0.02, Math.min(100, newZoom)); - - updateViewProjection(); - - // позиция после зума - const [postZoomX, postZoomY] = m3.transformPoint( - m3.inverse(viewProjectionMat), - [clipX, clipY]); - - // камеру нужно сдвинуть на разницу до и после - camera.x += preZoomX - postZoomX; - camera.y += preZoomY - postZoomY; -``` - -Обратите внимание, что зум — это противоположность scale. Если zoom = 2, то я хочу, чтобы всё выглядело в 2 раза больше. Для этого нужно *уменьшить* пространство камеры, то есть масштабировать его на 1 / zoom. - -Пример: - -{{{example url="../webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl-example-1.html"}}} - -Обратите внимание, что я добавил camera.rotation, чтобы убедиться, что всё работает и при повороте. Похоже, работает. [Вот пример с зумом, панорамой и вращением](https://jsfiddle.net/greggman/mdpxw3n6/) - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - nicktgn - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md deleted file mode 100644 index 8a5b80e9c..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md +++ /dev/null @@ -1,211 +0,0 @@ -Title: Как импортировать карту высот в WebGL -Description: Как импортировать карту высот в WebGL -TOC: Как импортировать карту высот в WebGL - -## Вопрос: - -Я знаю, что в теории нужно сначала найти координаты на карте высот, например (x = `ширина HM / ширина Terrain * x Terrain`) и y координату (`y = высота HM / высота Terrain * y Terrain`), и после получения местоположения на карте высот мы получаем реальную высоту по формуле `min_height + (colorVal / (max_color - min_color) * *max_height - min_height`), возвращая Z значение для конкретного сегмента. - -Но как я могу реально импортировать карту высот и получить её параметры? Я пишу на JavaScript без дополнительных библиотек (three, babylon). - -**edit** - -Сейчас я жёстко кодирую Z значения на основе диапазонов x и y: - - Plane.prototype.modifyGeometry=function(x,y){ - if((x>=0&&x<100)&&(y>=0&&y<100)){ - return 25; - } - else if((x>=100&&x<150)&&(y>=100&&y<150)){ - return 20; - } - else if((x>=150&&x<200)&&(y>=150&&y<200)){ - return 15; - } - else if((x>=200&&x<250)&&(y>=200&&y<250)){ - return 10; - } - else if((x>=250&&x<300)&&(y>=250&&y<300)){ - return 5; - } - else{ - return 0; - } - -** edit ** - -Я могу получить плоскую сетку (или с случайно сгенерированными высотами), но как только я добавляю данные изображения, получаю пустой экран (хотя ошибок нет). Вот код (я немного изменил его): - - - - var gl; - var canvas; - - var img = new Image(); - // img.onload = run; - img.crossOrigin = 'anonymous'; - img.src = 'https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png'; - - - var gridWidth; - var gridDepth; - var gridPoints = []; - var gridIndices = []; - var rowOff = 0; - var rowStride = gridWidth + 1; - var numVertices = (gridWidth * 2 * (gridDepth + 1)) + (gridDepth * 2 * (gridWidth + 1)); - - - //создание плоскости - function generateHeightPoints() { - - var ctx = document.createElement("canvas").getContext("2d"); //используем 2d canvas для чтения изображения - ctx.canvas.width = img.width; - ctx.canvas.height = img.height; - ctx.drawImage(img, 0, 0); - var imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); - - gridWidth = imgData.width - 1; - gridDepth = imgData.height - 1; - - for (var z = 0; z <= gridDepth; ++z) { - for (var x = 0; x <= gridWidth; ++x) { - var offset = (z * imgData.width + x) * 4; - var height = imgData.data[offset] * 10 / 255; - gridPoints.push(x, height, z); - } - } - } - - function generateIndices() { - for (var z = 0; z<=gridDepth; ++z) { - rowOff = z*rowStride; - for(var x = 0; x - -Читайте его, загружая изображение, рисуя на 2D canvas, вызывая getImageData. Затем просто читайте красные значения для высоты. - -{{{example url="../webgl-qna-how-to-import-a-heightmap-in-webgl-example-2.html"}}} - -Затем вместо создания сетки линий создайте сетку треугольников. Есть много способов это сделать. Вы можете поставить 2 треугольника на квадрат сетки. Этот код ставит 4. Вам также нужно генерировать нормали. Я скопировал код для генерации нормалей из [этой статьи](https://webglfundamentals.org/webgl/lessons/webgl-3d-geometry-lathe.html), который является довольно универсальным кодом генерации нормалей. Будучи сеткой, вы можете создать генератор нормалей, специфичный для сетки, который будет быстрее, поскольку будучи сеткой вы знаете, какие вершины общие. - -Этот код также использует [twgl](https://twgljs.org), потому что WebGL слишком многословен, но должно быть понятно, как сделать это в обычном WebGL, читая названия функций twgl. - -{{{example url="../webgl-qna-how-to-import-a-heightmap-in-webgl-example-3.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - cosmo - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md b/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md deleted file mode 100644 index d8c9e3a5c..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md +++ /dev/null @@ -1,70 +0,0 @@ -Title: Как загружать изображения в фоне без задержек -Description: Как загружать изображения в фоне без задержек -TOC: Как загружать изображения в фоне без задержек - -## Вопрос: - -В нашем WebGL приложении я пытаюсь загружать и декодировать текстуры в web worker, чтобы избежать задержек рендеринга в основном потоке. Использование createImageBitmap в worker и передача image bitmap обратно в основной поток работает хорошо, но в Chrome это использует **три** или больше (возможно, в зависимости от количества ядер?) отдельных workers (ThreadPoolForegroundWorker), которые вместе с основным потоком и моим собственным worker дают пять потоков. - -Я предполагаю, что это вызывает оставшиеся нарушения рендеринга на моём четырёхъядерном процессоре, поскольку я вижу необъяснимо долгие времена в функции Performance в DevTools Chrome. - -Итак, могу ли я как-то ограничить количество workers, используемых createImageBitmap? Даже если я передаю изображения как blobs или array buffers в основной поток и активирую createImageBitmap оттуда, его workers будут конкурировать с моим собственным worker и основным потоком. - -Я пробовал создавать обычные Images в worker вместо этого, чтобы явно декодировать их там, но Image не определён в контексте worker, как и document, если я хочу создать их как элементы. И обычные Images тоже не передаваемые, так что создание их в основном потоке и передача в worker тоже не кажется осуществимой. - -Жду любых предложений... - -## Ответ: - -Нет причин использовать createImageBitmap в worker (ну, см. внизу). Браузер уже декодирует изображение в отдельном потоке. Делать это в worker ничего не даёт. Большая проблема в том, что ImageBitmap не может знать, как вы собираетесь использовать изображение, когда наконец передадите его в WebGL. Если вы запросите формат, отличный от того, что декодировал ImageBitmap, то WebGL должен конвертировать и/или декодировать его снова, и вы не можете дать ImageBitmap достаточно информации, чтобы сказать ему, в каком формате вы хотите его декодировать. - -Кроме того, WebGL в Chrome должен передавать данные изображения из процесса рендеринга в GPU процесс, что для большого изображения является относительно большой копией (1024x1024 по RGBA это 4 мега) - -Лучший API, на мой взгляд, позволил бы вам сказать ImageBitmap, какой формат вы хотите и где вы хотите его (CPU, GPU). Таким образом браузер мог бы асинхронно подготовить изображение и не требовал бы тяжёлой работы при завершении. - -В любом случае, вот тест. Если вы снимите галочку "update texture", то текстуры всё ещё скачиваются и декодируются, но просто не вызывается `gl.texImage2D` для загрузки текстуры. В этом случае я не вижу задержек (не доказательство, что это проблема, но я думаю, что проблема там) - -{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-1.html"}}} - -Я довольно уверен, что единственный способ, которым вы могли бы гарантировать отсутствие задержек — это декодировать изображения самостоятельно в worker, передавать в основной поток как arraybuffer и загружать в WebGL по несколько строк за кадр с `gl.bufferSubData`. - -{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-2.html"}}} - -Примечание: Я не знаю, что это тоже будет работать. Несколько мест, которые пугают и зависят от реализации браузера - -1. Какие проблемы производительности при изменении размера canvas. Код изменяет размер OffscreenCanvas в worker. Это может быть тяжёлая операция с последствиями для GPU. - -2. Какая производительность рисования bitmap в canvas? Снова большие проблемы производительности GPU, поскольку браузер должен передать изображение на GPU, чтобы нарисовать его в GPU 2D canvas. - -3. Какая производительность getImageData? Снова браузер должен потенциально заморозить GPU, чтобы прочитать память GPU и получить данные изображения. - -4. Возможный удар по производительности при изменении размера текстуры. - -5. Только Chrome в настоящее время поддерживает OffscreenCanvas - -1, 2, 3 и 5 могут быть решены декодированием [jpg](https://github.com/notmasteryet/jpgjs), [png](https://github.com/arian/pngjs) изображения самостоятельно, хотя действительно отстойно, что браузер имеет код для декодирования изображения, просто вы не можете получить доступ к коду декодирования никаким полезным способом. - -Для 4, если это проблема, это может быть решено выделением текстуры максимального размера изображения и затем копированием меньших текстур в прямоугольную область. Предполагая, что это проблема - -{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-3.html"}}} - -примечание: jpeg декодер медленный. Если найдёте или сделаете более быстрый, пожалуйста, оставьте комментарий - ---- - -# Обновление - -Я просто хочу сказать, что `ImageBitmap` **должен** быть достаточно быстрым и что некоторые из моих комментариев выше о том, что у него недостаточно информации, могут быть не совсем правильными. - -Моё текущее понимание в том, что вся суть `ImageBitmap` была в том, чтобы сделать загрузки быстрыми. Это должно работать так: вы даёте ему blob и асинхронно он загружает это изображение в GPU. Когда вы вызываете `texImage2D` с ним, браузер может "blit" (рендерить с GPU) это изображение в вашу текстуру. Я не знаю, почему есть задержки в первом примере, учитывая это, но я вижу задержки каждые 6 или около того изображений. - -С другой стороны, хотя загрузка изображения в GPU была целью `ImageBitmap`, браузеры не обязаны загружать в GPU. `ImageBitmap` всё ещё должен работать, даже если у пользователя нет GPU. Суть в том, что браузер решает, как реализовать функцию, и будет ли она быстрой или медленной, без задержек или с ними — полностью зависит от браузера. - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - Andreas Ekstrand - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md b/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md deleted file mode 100644 index cd9169b2c..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md +++ /dev/null @@ -1,54 +0,0 @@ -Title: Как сделать инструмент размазывающей кисти -Description: Как сделать инструмент размазывающей кисти -TOC: Как сделать инструмент размазывающей кисти - -## Вопрос: - -Мне нужна идея, как я могу сделать кисть, которая может размазывать цвет. - -Пример на картинке: правая сторона - рисование базовой кистью с двумя разными цветами, слева также рисование, но дополнительно используется инструмент размазывания, **результат должен быть чем-то вроде левой стороны** - -![enter image description here][1] - -Мне нужен совет, как я могу попробовать это сделать - - [1]: http://i.stack.imgur.com/oyaBs.png - -## Ответ: - -Вот одна попытка - -1. При mousedown захватить копию области под мышью в отдельный canvas - -2. При mousemove рисовать эту копию по одному пикселю за раз от предыдущей позиции мыши к текущей позиции мыши с 50% прозрачностью, захватывая новую копию после каждого движения. - -В псевдокоде - -``` -on mouse down - grab copy of canvas at mouse position - prevMousePos = currentMousePos - -on mouse move - for (pos = prevMousePos to currentMousePos step 1 pixel) - draw copy at pos with 50% alpha - grab new copy of canvas at pos - prevMousePos = currentMousePos -``` - -Кисть размывается путем рисования радиального градиента от rgba(0,0,0,0) до rgba(0,0,0,1) над ней, используя `globalCompositeOperation = 'destination-out'`. - - -{{{example url="../webgl-qna-how-to-make-a-smudge-brush-tool-example-1.html"}}} - - - - -
-
Вопрос и процитированные части являются - CC BY-SA 3.0 от - Your choice - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md b/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md deleted file mode 100644 index 7ef38b3ac..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md +++ /dev/null @@ -1,62 +0,0 @@ -Title: Как сделать canvas WebGL прозрачным -Description: Как сделать canvas WebGL прозрачным -TOC: Как сделать canvas WebGL прозрачным - -## Вопрос: - -Можно ли сделать **WebGL canvas** с прозрачным фоном? -Я хочу, чтобы содержимое веб-страницы было видно сквозь canvas. - -Вот что у меня сейчас: http://i50.tinypic.com/2vvq7h2.png - -Как видно, текст за WebGL canvas не виден. Когда я меняю стиль элемента Canvas в CSS и добавляю - - opacity: 0.5; - -Страница выглядит так: -https://imgur.com/BMgHWsZ - -Что почти то, что мне нужно, но не совсем — цвет текста из-за CSS alpha, конечно, не такой же черный, а цвет синей фигуры не такой же синий, как на первом изображении. - -Спасибо за любую помощь! -https://imgur.com/hSu5tyM - -## Ответ: - -WebGL по умолчанию поддерживает прозрачность. Вот пример - -{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-1.html"}}} - -Обратите внимание, что браузер предполагает, что пиксели в canvas представлены в формате PRE-MULTIPLIED-ALPHA. Это значит, например, если вы измените цвет очистки на (1, 0, 0, 0.5), вы получите то, что не увидите больше нигде в HTML. - -Я имею в виду, что pre-multiplied alpha означает, что RGB-части уже умножены на alpha. То есть если у вас 1,0,0 для RGB и alpha 0.5, то после умножения RGB на alpha получится 0.5, 0, 0 для RGB. Именно это браузер ожидает по умолчанию. - -Если пиксели в WebGL — 1,0,0,0.5, это не имеет смысла для браузера, и вы получите странные эффекты. - -Смотрите, например: - -{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-2.html"}}} - -Обратите внимание, что черный текст стал красным, хотя вы могли бы подумать, что alpha 0.5 = 50% черного текста и 50% красного WebGL canvas. Это потому, что красный не был pre-multiplied. - -Вы можете решить это, убедившись, что значения, которые вы создаете в WebGL, представляют собой pre-multiplied значения, или вы можете сказать браузеру, что ваши пиксели WebGL не pre-multiplied, когда создаете контекст webgl так: - - const gl = canvas.getContext("webgl", { premultipliedAlpha: false }); - -Теперь пиксели 1,0,0,0.5 снова работают. Пример: - -{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-3.html"}}} - -Какой способ использовать — зависит от вашего приложения. Многие GL-программы ожидают не pre-multiplied alpha, тогда как все остальные части HTML5 ожидают pre-multiplied alpha, поэтому WebGL дает вам оба варианта. - - - - -
-
Вопрос и процитированные части являются - CC BY-SA 3.0 от - Jack Sean - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md b/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md deleted file mode 100644 index 1ed856cc1..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md +++ /dev/null @@ -1,115 +0,0 @@ -Title: Как оптимизировать рендеринг UI -Description: Как оптимизировать рендеринг UI -TOC: Как оптимизировать рендеринг UI - -## Вопрос: - -Я только начинаю с WebGL. Я следовал простому руководству для начинающих на YouTube. Теперь я пытаюсь создать простую 2D игру. - -В этой игре я хочу рендерить простой инвентарь с изображениями. Когда я это делаю, мой FPS падает до 2 после 10 секунд. Если я убираю код для рендеринга инвентаря, он остаётся на 60. - -Я знаю, что моя проблема в строке 82 в `game/js/engine/inventory/inventory.js`. Там я рендерю 35 изображений с классом sprite, который я сделал, смотря руководство. Я думаю, что поскольку я смотрел простое руководство, код, который рендерит изображение, не оптимизирован и, вероятно, не лучший способ это делать. Класс sprite находится в `game/js/engine/material.js:127`. В классе sprite я настраиваю простые переменные, которые можно передать в мой вершинный и фрагментный шейдер. - -## Настройка Sprite ## -В методе setup я настраиваю все параметры для моего изображения. -``` -gl.useProgram(this.material.program); - -this.gl_tex = gl.createTexture(); - -gl.bindTexture(gl.TEXTURE_2D, this.gl_tex); -gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); -gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); -gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); -gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); -gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image); -gl.bindTexture(gl.TEXTURE_2D, null); - -this.uv_x = this.size.x / this.image.width; -this.uv_y = this.size.y / this.image.height; - -this.tex_buff = gl.createBuffer(); -gl.bindBuffer(gl.ARRAY_BUFFER, this.tex_buff); -gl.bufferData(gl.ARRAY_BUFFER, Sprite.createRenderRectArray(0, 0, this.uv_x, this.uv_y), gl.STATIC_DRAW); - -this.geo_buff = gl.createBuffer(); -gl.bindBuffer(gl.ARRAY_BUFFER, this.geo_buff); -gl.bufferData(gl.ARRAY_BUFFER, Sprite.createRectArray(0, 0, this.size.x, this.size.y), gl.STATIC_DRAW); - -gl.useProgram(null); -``` - -## Рендеринг Sprite ## -В методе render я сначала привязываю текстуру. Затем привязываю буфер текстурных координат, геометрический буфер и некоторые смещения для моего мира. Наконец, рисую массивы. -``` -let frame_x = Math.floor(frames.x) * this.uv_x; -let frame_y = Math.floor(frames.y) * this.uv_y; - -let oMat = new M3x3().transition(position.x, position.y); -gl.useProgram(this.material.program); - -this.material.set("u_color", 1, 1, 1, 1); - -gl.activeTexture(gl.TEXTURE0); -gl.bindTexture(gl.TEXTURE_2D, this.gl_tex); -this.material.set("u_image", 0); - -gl.bindBuffer(gl.ARRAY_BUFFER, this.tex_buff); -this.material.set("a_texCoord"); - -gl.bindBuffer(gl.ARRAY_BUFFER, this.geo_buff); -this.material.set("a_position"); - -this.material.set("u_texeloffset", 0.5 / (this.image.width * scale.x), 0.5 / (this.image.height * scale.y)); -this.material.set("u_frame", frame_x, frame_y); -this.material.set("u_world", worldSpaceMatrix.getFloatArray()); -this.material.set("u_object", oMat.getFloatArray()); - -gl.drawArrays(gl.TRIANGLE_STRIP, 0, 6); -gl.useProgram(null); -``` -Github: [https://github.com/DJ1TJOO/2DGame/][1] - -У кого-нибудь есть идея, как я могу исправить/оптимизировать это? -Или может быть есть лучший способ рендерить инвентарь? - -Если вы найдёте любой другой способ улучшить мой WebGL или JavaScript, пожалуйста, скажите. - -[1]: https://github.com/DJ1TJOO/2DGame/ - -## Ответ: - -> есть лучший способ рендерить инвентарь? - -Есть несколько способов оптимизации, которые приходят в голову. - -1. Может быть быстрее просто использовать HTML для вашего инвентаря - - Серьёзно: Вы также получаете простое международное рендеринг шрифтов, стили, - отзывчивость с CSS и т.д... Много игр делают это. - -2. Обычно быстрее использовать texture atlas (одну текстуру с множеством разных изображений), затем генерировать вершины в vertex buffer для всех частей вашего инвентаря. Затем рисовать всё одним вызовом draw. Так работает, например, [Dear ImGUI](https://github.com/ocornut/imgui), чтобы делать [все эти удивительные GUI](https://github.com/ocornut/imgui/issues/3075). Он сам ничего не рисует, он просто генерирует vertex buffer с позициями и текстурными координатами для texture atlas. - -3. Делайте #2, но вместо генерации всего vertex buffer каждый кадр просто обновляйте части, которые изменяются. - - Так, например, допустим ваш инвентарь показывает - - [gold ] 123 - [silver] 54 - [copper] 2394 - - Допустим, вы всегда рисуете `[gold ]`, `[silver]` и `[copper]`, но только числа изменяются. Вы могли бы генерировать vertex buffers, которые содержат все позиции для каждой буквы как sprite, и затем сказать 6 placeholder'ов символов для каждого значения. Вам нужно обновлять только числа, когда они изменяются, помня, где они находятся в vertex buffers. Для любой цифры, которую вы не хотите рисовать, вы можете просто переместить её вершины за экран. - -4. Рисуйте инвентарь в текстуру (или части его). Затем рисуйте текстуру на экране. Обновляйте только части текстуры, которые изменяются. - - Это в основном [то, что делает сам браузер](https://www.html5rocks.com/en/tutorials/speed/layers/). На основе различных настроек CSS части страницы разделяются на текстуры. Когда какой-то HTML или CSS изменяется, только те текстуры, в которых что-то изменилось, перерисовываются, а затем все текстуры рисуются для рекомпозиции страницы. - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - DJ1TJOO - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md b/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md deleted file mode 100644 index 7c462415a..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md +++ /dev/null @@ -1,125 +0,0 @@ -Title: Как предотвратить просачивание текстур с атласом текстур -Description: Как предотвратить просачивание текстур с атласом текстур -TOC: Как предотвратить просачивание текстур с атласом текстур - -## Вопрос: - -Я применил два необходимых шага, указанных в этом ответе https://gamedev.stackexchange.com/questions/46963/how-to-avoid-texture-bleeding-in-a-texture-atlas, но я всё ещё получаю просачивание текстур. - -У меня есть атлас, который заполнен сплошными цветами на границах: `x y w h: 0 0 32 32, 0 32 32 32, 0 64 32 32, 0 32 * 3 32 32` - -Я хочу отображать каждый из этих кадров, используя WebGL без просачивания текстур, только сплошные цвета как есть. - -Я отключил мипмаппинг: - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - - //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - -Я применил коррекцию полпикселя: - - const uvs = (src, frame) => { - const tw = src.width, - th = src.height; - - const getTexelCoords = (x, y) => { - return [(x + 0.5) / tw, (y + 0.5) / th]; - }; - - let frameLeft = frame[0], - frameRight = frame[0] + frame[2], - frameTop = frame[1], - frameBottom = frame[1] + frame[3]; - - let p0 = getTexelCoords(frameLeft, frameTop), - p1 = getTexelCoords(frameRight, frameTop), - p2 = getTexelCoords(frameRight, frameBottom), - p3 = getTexelCoords(frameLeft, frameBottom); - - return [ - p0[0], p0[1], - p1[0], p1[1], - p3[0], p3[1], - p2[0], p2[1] - ]; - }; - -Но я всё ещё получаю просачивание текстур. Сначала я попробовал использовать pixi.js, и я тоже получил просачивание текстур, затем я попробовал использовать vanilla js. - -Я исправил это, изменив эти строки: - - let frameLeft = frame[0], - frameRight = frame[0] + frame[2] - 1, - frameTop = frame[1], - frameBottom = frame[1] + frame[3] - 1; - -Как вы можете видеть, я вычитаю 1 из правого и нижнего краёв. Ранее эти индексы были 32, что означает начало другого кадра, это должно быть 31 вместо этого. Я не знаю, является ли это правильным решением. - -## Ответ: - -Ваше решение правильно. - -Представьте, что у нас есть текстура 4x2 с двумя спрайтами 2x2 пикселя - -``` -+-------+-------+-------+-------+ -| | | | | -| E | F | G | H | -| | | | | -+-------+-------+-------+-------+ -| | | | | -| A | B | C | D | -| | | | | -+-------+-------+-------+-------+ -``` - -Буквы представляют центры пикселей в текстурах. - -``` -(pixelCoord + 0.5) / textureDimensions -``` - -Возьмите спрайт 2x2 в A, B, E, F. Если ваши текстурные координаты идут где-либо между B и C, то вы получите некоторую смесь C, если у вас включена фильтрация текстур. - -Изначально вы вычисляли координаты A, A + width, где width = 2. Это привело вас от A до C. Добавив -1, вы получаете только от A до B. - -К сожалению, у вас есть новая проблема, которая заключается в том, что вы отображаете только половину A и B. Вы можете решить это, добавив отступы к спрайтам. Например, сделайте его 6x2 с пикселем между ними, который повторяет края спрайта - -``` -+-------+-------+-------+-------+-------+-------+ -| | | | | | | -| E | F | Fr | Gr | G | H | -| | | | | | | -+-------+-------+-------+-------+-------+-------+ -| | | | | | | -| A | B | Br | Cr | C | D | -| | | | | | | -+-------+-------+-------+-------+-------+-------+ -``` - -Выше Br - это B повторённый, Cr - это C повторённый. Установка repeat как `gl.CLAMP_TO_EDGE` повторит A и D для вас. Теперь вы можете использовать края. - -Координаты спрайта CDGH: - - p0 = 4 / texWidth - p1 = 0 / texHeigth - p2 = (4 + spriteWidth) / texWidth - p3 = (0 + spriteHeigth) / texHeight - -Лучший способ увидеть разницу - нарисовать 2 спрайта крупно, используя обе техники, без отступов и с отступами. - -{{{example url="../webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas-example-1.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - eguneys - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md b/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md deleted file mode 100644 index ff3cf2acb..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md +++ /dev/null @@ -1,42 +0,0 @@ -Title: Как обрабатывать позиции частиц -Description: Как обрабатывать позиции частиц -TOC: Как обрабатывать позиции частиц - -## Вопрос: - -Используя сетку 4x4x4 в качестве примера, у меня есть 64 вершины (которые я буду называть частицами), которые начинают с определённых позиций относительно друг друга. Эти 64 частицы будут двигаться в направлениях x, y и z, теряя свои начальные позиции относительно друг друга. Однако каждый цикл новые позиции частиц и скорости должны вычисляться на основе исходных начальных отношений между частицей и её исходными соседями. - -Я узнал, что мне нужно использовать текстуры и, следовательно, Framebuffers для этого, и теперь я могу записывать две 3DTextures, которые переключаются, чтобы обеспечить функциональность записи и чтения для выполнения этого. Однако в следующем цикле, когда gl_FragCoord передаётся в фрагментный шейдер с новой позицией частицы (которая может быть переключена с другой частицей, например), я не вижу никакого механизма, с помощью которого исходная координата текстуры, которая содержала информацию о частице, будет записана с текущей информацией частицы. Есть ли какой-то механизм, который я не понимаю, который позволяет движущимся частицам иметь свои данные, хранящиеся в статической сетке (3D текстура), с данными каждой частицы, всегда заполняющими ту же координату, чтобы я мог использовать texelFetch для получения данных частицы, а также данных исходных соседей? Могу ли я изменить gl_FragCoord и иметь вывод пикселя там, где я хочу, или это неизменяемая входная переменная? - -Как только я решу эту проблему, я надеюсь затем реализовать Transform Feedback для выполнения фактического движения вершин без сброса текстуры в CPU и извлечения данных позиции и повторной загрузки их в GPU для следующего цикла. - -Есть ли какие-либо предложения о том, как отслеживать исходную позицию каждой частицы, исходных соседей и текущую позицию относительно этих исходных соседей, используя текстуры, записанные в Framebuffers? - -## Ответ: - -Я запутался в вашей путанице - -Вот простая система частиц только на JavaScript. Каждая частица начинается в случайном месте и движется в случайном направлении - -{{{example url="../webgl-qna-how-to-process-particle-positions-example-1.html"}}} - -Вот та же система частиц, всё ещё в JavaScript, но работающая больше как работает WebGL. Я не знаю, будет ли это более или менее запутанно. Важные моменты заключаются в том, что код, который обновляет позиции частиц, называемый `fragmentShader`, не может выбирать, что он обновляет. Он просто обновляет `gl.outColor`. У него также нет входов, кроме `gl.fragCoord` и `gl.currentProgram.uniforms`. currentParticleState - это массив массивов из 4 значений, где, как и раньше, это был массив объектов со свойством position. particleParameters также просто массив массивов из 4 значений вместо массива объектов со значением velocity. Это для имитации того факта, что это были бы текстуры в реальном WebGL, поэтому любое значение, такое как `position` или `velocity`, теряется. - -Код, который фактически рисует частицы, не имеет значения. - -{{{example url="../webgl-qna-how-to-process-particle-positions-example-2.html"}}} - -Вот тот же код в реальном WebGL - -{{{example url="../webgl-qna-how-to-process-particle-positions-example-3.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - billvh - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md b/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md deleted file mode 100644 index a6df3a16c..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md +++ /dev/null @@ -1,295 +0,0 @@ -Title: Как читать один компонент с помощью readPixels -Description: Как читать один компонент с помощью readPixels -TOC: Как читать один компонент с помощью readPixels - -## Вопрос: - -Я конвертировал RGBA изображение в оттенки серого с помощью WebGL. - -При чтении пикселей с помощью `gl.readPixels()` с форматом `gl.RGBA` получаю значения для каждого пикселя как YYYA, потому что RGBA пиксель конвертируется в YYYA и присваивается `gl_FragColor`. Я хочу только 1 байт Y-компонента для каждого пикселя вместо 4 байт. - -Попробовал читать пиксели с форматом `gl.RED` (вместо `gl.RGBA`) - -``` -gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RED, gl.UNSIGNED_BYTE, pixels); -``` - -но получаю следующую ошибку в Chrome и только нули: - -``` -WebGL: INVALID_ENUM: readPixels: invalid format -``` - -1. Можно ли заставить `gl_FragColor` выводить 1 байт на пиксель в режиме LUMINANCE вместо RGBA, но входная текстура должна быть RGBA? -2. Если формат рендеринга gl нельзя изменить, можно ли читать только первый байт каждого 4-байтового пикселя при вызове `gl.readPixels()`? - -Примечание: -3. Я уже делаю копию вывода `gl.readPixels()` в другой массив, перепрыгивая каждые 4 байта. Но хочу избежать этой копии, так как это занимает больше времени. -4. Также нужно, чтобы решение было совместимо с мобильными браузерами (iOS Safari и Android Chrome). - - - - - - function webGL() { - var gTexture; - var gFramebuffer; - var srcCanvas = null; - var programs = {}; - var program; - var pixels; - - this.convertRGBA2Gray = function(inCanvas, inArray) { - // Y компонент из YCbCr - const shaderSourceRGB2GRAY = ` - precision mediump float; - - uniform sampler2D u_image; - uniform vec2 u_textureSize; - vec4 scale = vec4(0.299, 0.587, 0.114, 0.0); - void main() { - vec4 color = texture2D(u_image, gl_FragCoord.xy / u_textureSize); - gl_FragColor = vec4(vec3(dot(color,scale)), color.a); - }`; - - if (srcCanvas === null) { - console.log('Setting up webgl'); - srcCanvas = inCanvas; - _initialize(srcCanvas.width, srcCanvas.height); - program = _createProgram("rgb2grey", shaderSourceRGB2GRAY); - } - pixels = inArray; - _run(program); - } - - /////////////////////////////////////// - // Приватные функции - - var _getWebGLContext = function(canvas) { - try { - return (canvas.getContext("webgl", {premultipliedAlpha: false, preserveDrawingBuffer: true}) || canvas.getContext("experimental-webgl", {premultipliedAlpha: false, preserveDrawingBuffer: true})); - } - catch(e) { - console.log("ERROR: %o", e); - } - return null; - } - - var gl = _getWebGLContext(document.createElement('canvas')); - - var _initialize = function(width, height) { - var canvas = gl.canvas; - canvas.width = width; - canvas.height = height; - - if (this.originalImageTexture) { - return; - } - - this.originalImageTexture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); - - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - - gTexture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, gTexture); - - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - - gl.texImage2D( - gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, - gl.RGBA, gl.UNSIGNED_BYTE, null); - - gFramebuffer = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, gFramebuffer); - - var positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ - -1.0, 1.0, - 1.0, 1.0, - 1.0, -1.0, - - -1.0, 1.0, - 1.0, -1.0, - -1.0, -1.0 - ]), gl.STATIC_DRAW); - - gl.framebufferTexture2D( - gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, gTexture, 0); - - gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, srcCanvas); - } - - var _createProgram = function(name, fragmentSource, vertexSource) { - shaderProgram = programs[name]; - - if (shaderProgram){ - console.log('Reusing program'); - gl.useProgram(shaderProgram); - return shaderProgram; - } - - function createShader(type, source){ - var shader = gl.createShader(type); - - gl.shaderSource(shader, source); - - gl.compileShader(shader); - - return shader; - } - - var vertexShader, fragmentShader; - - if (!vertexSource){ - vertexShader = createShader(gl.VERTEX_SHADER, `attribute vec2 a_position; - void main() { gl_Position = vec4(a_position, 0.0, 1.0); }` - ); - } else { - vertexShader = createShader(gl.VERTEX_SHADER, vertexSource); - } - fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource); - - shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - gl.useProgram(shaderProgram); - return shaderProgram; - } - - var _render = function(gl, program){ - var positionLocation = gl.getAttribLocation(program, "a_position"); - - var u_imageLoc = gl.getUniformLocation(program, "u_image"); - var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize"); - - gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(positionLocation); - - var width = gl.canvas.width, - height = gl.canvas.height; - - gl.bindFramebuffer(gl.FRAMEBUFFER, gFramebuffer); - - gl.uniform2f(textureSizeLocation, width, height); - - gl.uniform1i(u_imageLoc, 0); - - gl.viewport(0, 0, width, height); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - } - - var _run = function(program){ - let t0 = performance.now(); - _render(gl, program); - gl.bindTexture(gl.TEXTURE_2D, gTexture); - let t1 = performance.now(); - - // gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RED, gl.UNSIGNED_BYTE, pixels); - - let t2 = performance.now(); - console.log('_render dur = ' + Number((t1-t0).toFixed(3)) + ' ms'); - console.log('_run dur = ' + Number((t2-t0).toFixed(3)) + ' ms'); - } - - }; - - - - -
- -
- - - - - - - - - - -## Ответ: - -> Можно ли заставить gl_FragColor выводить 1 байт на пиксель в режиме LUMINANCE вместо RGBA, но входная текстура должна быть RGBA? - -Не переносимо. Спецификация WebGL1 говорит, что рендеринг в текстуру должен поддерживаться только для gl.RGBA / gl.UNSIGNED_BYTE. Все остальные форматы опциональны. - -> Если формат рендеринга gl нельзя изменить, можно ли читать только первый байт каждого 4-байтового пикселя при вызове gl.readPixels()? - -Нет, [Спецификация](https://www.khronos.org/registry/OpenGL/specs/es/2.0/es_full_spec_2.0.pdf) раздел 4.3.1 говорит, что поддерживается только `gl.RGBA`, `gl.UNSIGNED_BYTE`. Все остальные форматы опциональны и зависят от реализации. То же самое в WebGL2. Даже если вы создадите R8 текстуру (только красный, 8 бит), зависит от реализации, можете ли вы читать её как `gl.RED`/`gl.UNSIGNED_BYTE`. - -Смотрите [Webgl1](https://webglfundamentals.org/webgl/lessons/webgl-readpixels.html) и [Webgl2](https://webgl2fundamentals.org/webgl/lessons/webgl-readpixels.html) - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - rayen - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md b/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md deleted file mode 100644 index e61c45f9d..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md +++ /dev/null @@ -1,113 +0,0 @@ -Title: Как рендерить изображения большого масштаба как 32000x32000 -Description: Как рендерить изображения большого масштаба как 32000x32000 -TOC: Как рендерить изображения большого масштаба как 32000x32000 - -## Вопрос: - -Я хочу получить снимок моего WebGL canvas, и я хочу высокое разрешение, поэтому я увеличил размер моего canvas. Это автоматически изменяет `gl.drawingBufferWidth` и `gl.drawingBufferHeight`. Затем я устанавливаю viewport и рендерю сцену. - -Мой код работает правильно в низком разрешении (под 4000*4000), но в более высоких разрешениях есть много проблем. - -Если разрешение немного выше, снимок не показывает полностью. См. прикреплённый файл. Если разрешение увеличивается больше, ничего не показывается. И наконец, при некоторых разрешениях мой экземпляр WebGL уничтожается, и мне приходится перезапускать браузер, чтобы снова запустить WebGL. - -Есть ли способ получить снимок с WebGL canvas с высоким разрешением? Могу ли я использовать другое решение? - -## Ответ: - -4000x4000 пикселей - это 4000x4000x4 или 64 мегабайта памяти. 8000x8000 - это 256 мегабайт памяти. Браузеры не любят выделять такие большие куски памяти и часто устанавливают лимиты на страницу. Так, например, у вас есть WebGL canvas 8000x8000, который требует 2 буфера. Drawingbuffer И текстура, отображаемая на странице. Drawingbuffer может быть сглажен. Если это 4x MSAA, то потребуется гигабайт памяти только для этого буфера. Затем вы делаете скриншот, так что ещё 256 мегабайт памяти. Так что да, браузер по той или иной причине, скорее всего, убьёт вашу страницу. - -Помимо этого, WebGL имеет свои собственные лимиты в размере. Вы можете посмотреть этот лимит, который эффективно [`MAX_TEXTURE_SIZE`](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE) или [`MAX_VIEWPORT_DIMS`](https://web3dsurvey.com/webgl/parameters/MAX_VIEWPORT_DIMS). Вы можете увидеть из тех, что около 40% машин не могут рисовать больше 4096 (хотя если вы [отфильтруете только десктоп, это намного лучше](https://web3dsurvey.com/webgl/parameters/MAX_VIEWPORT_DIMS?platforms=0000ff03c02d20f201)). Это число означает только то, что может делать оборудование. Оно всё ещё ограничено памятью. - -Один способ, который может решить эту проблему, - рисовать изображение по частям. Как вы это делаете, будет зависеть от вашего приложения. Если вы используете довольно стандартную матрицу перспективы для всего вашего рендеринга, вы можете использовать немного другую математику для рендеринга любой части вида. Большинство 3D математических библиотек имеют функцию `perspective`, и большинство из них также имеют соответствующую функцию `frustum`, которая немного более гибкая. - -Вот довольно стандартный стиль WebGL простой пример, который рисует куб, используя типичную функцию `perspective` - -{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-1.html"}}} - -И вот тот же код, рендерящий в 400x200 в восьми частях 100x100, используя типичную функцию `frustum` вместо `perspective` - -{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-2.html"}}} - -Если вы запустите фрагмент выше, вы увидите, что он генерирует 8 изображений - -Важные части - это - -Сначала нам нужно решить общий размер, который мы хотим - - const totalWidth = 400; - const totalHeight = 200; - -Затем мы создадим функцию, которая будет рендерить любую меньшую часть этого размера - - function renderPortion(totalWidth, totalHeight, partX, partY, partWidth, partHeight) { - ... - -Мы установим canvas на размер части - - gl.canvas.width = partWidth; - gl.canvas.height = partHeight; - - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - -И затем вычислим, что нам нужно передать в функцию `frustum`. Сначала мы вычисляем прямоугольник на zNear, который матрица перспективы создала бы, учитывая наши значения field of view, aspect и zNear - - // углы на zNear для общего изображения - const zNearTotalTop = Math.tan(fov) * 0.5 * zNear; - const zNearTotalBottom = -zNearTotalTop; - const zNearTotalLeft = zNearTotalBottom * aspect; - const zNearTotalRight = zNearTotalTop * aspect; - - // ширина, высота на zNear для общего изображения - const zNearTotalWidth = zNearTotalRight - zNearTotalLeft; - const zNearTotalHeight = zNearTotalTop - zNearTotalBottom; - -Затем мы вычисляем соответствующую область на zNear для части, которую мы хотим рендерить, и передаём их в `frustum` для генерации матрицы проекции. - - const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth; const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth; - const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight; - const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight; - - const projection = m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar); - -Затем мы просто рендерим как обычно - -Наконец, снаружи у нас есть цикл для использования функции, которую мы только что сгенерировали, чтобы рендерить столько частей, сколько мы хотим, с любым разрешением, которое мы хотим. - - const totalWidth = 400; - const totalHeight = 200; - const partWidth = 100; - const partHeight = 100; - - for (let y = 0; y < totalHeight; y += partHeight) { - for (let x = 0; x < totalWidth; x += partWidth) { - renderPortion(totalWidth, totalHeight, x, y, partWidth, partHeight); - const img = new Image(); - img.src = gl.canvas.toDataURL(); - // сделать что-то с изображением. - } - } - -Это позволит вам рендерить в любой размер, который вы хотите, но вам понадобится другой способ собрать изображения в одно большее изображение. Вы можете или не можете сделать это в браузере. Вы можете попробовать создать гигантский 2D canvas и рисовать каждую часть в него (это предполагает, что 2D canvas не имеет тех же лимитов, что и WebGL). Для этого нет необходимости создавать изображения, просто рисуйте WebGL canvas в 2D canvas. - -В противном случае вам, возможно, придётся отправить их на сервер, который вы создадите, чтобы собрать изображение, или в зависимости от вашего случая использования позволить пользователю сохранить их и загрузить их все в программу редактирования изображений. - -Или если вы просто хотите отобразить их, браузер, вероятно, будет лучше работать с 16x16 изображениями 1024x1024, чем с одним изображением 16kx16k. В этом случае вы, вероятно, хотите вызвать `canvas.toBlob` вместо использования dataURL и затем вызвать `URL.createObjectURL` для каждого blob. Таким образом, у вас не будет этих гигантских строк dataURL. - -Пример: - -{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-3.html"}}} - -Если вы хотите, чтобы пользователь мог скачать изображение 16386x16386 вместо 256 изображений 1024x1024, то ещё одно решение - использовать код рендеринга частей выше и для каждой строки (или строк) изображений записать их данные в blob для ручной генерации PNG. [Этот пост в блоге](https://medium.com/the-guardian-mobile-innovation-lab/generating-images-in-javascript-without-using-the-canvas-api-77f3f4355fad) охватывает ручную генерацию PNG из данных, и [этот ответ предлагает, как сделать это для очень больших данных](https://stackoverflow.com/a/51247740/128511). - -## обновление: - -Просто для развлечения я написал эту [библиотеку, чтобы помочь генерировать гигантские PNG в браузере](https://github.com/greggman/dekapng). - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - MHA15 - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md deleted file mode 100644 index 000bf26db..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md +++ /dev/null @@ -1,105 +0,0 @@ -Title: Как симулировать 3D текстуру в WebGL -Description: Как симулировать 3D текстуру в WebGL -TOC: Как симулировать 3D текстуру в WebGL - -## Вопрос: - -Итак, в WebGL я могу хранить текстуру максимум в 2 измерениях и читать её в шейдере с помощью texture2D(whatever); - -Если я хочу хранить 3-мерную текстуру, чтобы читать 3-мерные данные в шейдере, как это сделать? - -Вот мои идеи — и я хочу знать, правильно ли я подхожу к этому: - -В JavaScript: - - var info = []; - - for (var x = 0; x < 1; x+=.1) { - for (var y = 0; y < 1; y+=.1) { - for (var z = 0; z < 1; z+=.1) { - - info.push (x*y*z); - info.push(0); - info.push(0); - info.push(0); - - } - } - } - - //bind texture here- whatever - - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 10, 100, 0, - gl.RGBA, gl.FLOAT, data_on_shader); - - //other texture stuff - -В шейдере: - - uniform sampler data_on_shader; - x= texture.r// - y = texture.g// - z = texture.b// - - xfixed = floor(x*10.)/10. + .5; - yfixed = floor(y*10.)/10. + .5; - zfixed = floor(z*10.)/10. + .5; - - float data_received = texture2D(data_on_shader, vec2(xfixed, yfixed*10. + zfixed)).r; - -Что-то вроде использования row major order в 2D текстуре? -Мысли? - -Спасибо заранее! - -## Ответ: - -Вы можете симулировать 3D текстуру, сохраняя каждую плоскость 3D текстуры в 2D текстуре - -Затем функция вроде этой позволит вам использовать её как 3D текстуру: - - vec4 sampleAs3DTexture(sampler2D tex, vec3 texCoord, float size) { - float sliceSize = 1.0 / size; // пространство 1 среза - float slicePixelSize = sliceSize / size; // пространство 1 пикселя - float sliceInnerSize = slicePixelSize * (size - 1.0); // пространство size пикселей - float zSlice0 = min(floor(texCoord.z * size), size - 1.0); - float zSlice1 = min(zSlice0 + 1.0, size - 1.0); - float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize; - float s0 = xOffset + (zSlice0 * sliceSize); - float s1 = xOffset + (zSlice1 * sliceSize); - vec4 slice0Color = texture2D(tex, vec2(s0, texCoord.y)); - vec4 slice1Color = texture2D(tex, vec2(s1, texCoord.y)); - float zOffset = mod(texCoord.z * size, 1.0); - return mix(slice0Color, slice1Color, zOffset); - } - -Если ваша 3D текстура была 8x8x8, то вы создаёте 2D текстуру 64x8 и помещаете каждую плоскость 3D текстуры в вашу 2D текстуру. Затем, зная, что изначально было 8x8x8, вы передаёте `8.0` для размера в `sampleAs3DTexture` - - precision mediump float; - uniform sampler2D u_my3DTexture; - varying vec3 v_texCoord; - - ... - - #define CUBE_SIZE 8.0 - - void main() { - gl_FragColor = sampleAs3DTexture(u_my3DTexture, v_texCoord, CUBE_SIZE); - } - -Примечание: функция выше предполагает, что вы хотите билинейную фильтрацию между плоскостями. Если не хотите, можете упростить функцию. - -Есть [видео-объяснение этого кода здесь][1], которое взято из [этого примера][2]. - - - [1]: http://www.youtube.com/watch?v=rfQ8rKGTVlg#t=26m00s - [2]: http://webglsamples.googlecode.com/hg/color-adjust/color-adjust.html - -
-
Вопрос и процитированные части являются - CC BY-SA 3.0 от - Skorpius - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md b/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md deleted file mode 100644 index 037476ad2..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md +++ /dev/null @@ -1,97 +0,0 @@ -Title: Как поддерживать и WebGL, и WebGL2 -Description: Как поддерживать и WebGL, и WebGL2 -TOC: Как поддерживать и WebGL, и WebGL2 - -## Вопрос: - -У меня есть библиотека, которая использует WebGL1 для рендеринга. -Она активно использует float-текстуры и instanced rendering. - -Сейчас поддержка WebGL1 довольно странная: некоторые устройства поддерживают, например, WebGL2 (где эти расширения встроены), но не поддерживают WebGL1, или поддерживают, но без нужных расширений. - -В то же время поддержка WebGL2 не идеальна. Возможно, когда-нибудь будет, но пока нет. - -Я начал думать, что потребуется для поддержки обеих версий. - -Для шейдеров, думаю, можно обойтись `#define`-ами. Например, `#define texture2D texture` и подобные вещи. - -С расширениями сложнее, так как объекты расширений больше не существуют. -В качестве эксперимента я пробовал копировать методы расширения в сам контекст, например: `gl.drawArraysInstanced = (...args) => ext.drawArraysInstancedANGLE(...args)`. - -С текстурами менять почти ничего не нужно, разве что добавить что-то вроде `gl.RGBA8 = gl.RGBA` при запуске в WebGL1, чтобы код "просто работал" и в WebGL2. - -Вопрос: кто-нибудь пробовал так делать? -Я опасаюсь, что это повредит производительности, особенно из-за лишней обёртки для вызова функций. -Это также усложнит чтение кода, если предполагать, что он может работать в WebGL1. Ведь в WebGL1 нет `drawArraysInstanced` или `RGBA8`. Это также мешает типизации в Typescript и другим мелочам. - -Второй вариант — делать ветвления по всему коду. Две версии шейдеров (или `#ifdef`-трюки), много ветвлений для каждого места, где нужны форматы текстур, и для каждого места, где используется instancing. -Вот так по всему коду — не очень красиво: - - if (version === 1) { - instancedArrays.vertexAttribDivisorANGLE(m0, 1); - instancedArrays.vertexAttribDivisorANGLE(m1, 1); - instancedArrays.vertexAttribDivisorANGLE(m2, 1); - instancedArrays.vertexAttribDivisorANGLE(m3, 1); - } else { - gl.vertexAttribDivisor(m0, 1); - gl.vertexAttribDivisor(m1, 1); - gl.vertexAttribDivisor(m2, 1); - gl.vertexAttribDivisor(m3, 1); - } - -Может, есть и третий способ, о котором я не подумал. - -Есть рекомендации? - -## Ответ: - -К сожалению, большинство ответов тут будут субъективными. - -Первый вопрос — зачем поддерживать оба? Если ваша идея работает в WebGL1 — просто используйте WebGL1. Если вам обязательно нужны фичи WebGL2 — используйте WebGL2 и смиритесь, что на старых устройствах не будет поддержки. - -Если вы всё же хотите поддерживать оба, [twgl](https://twgljs.org) пытается упростить это, [предлагая функцию, которая копирует все расширения WebGL1 в их WebGL2 API-позиции](http://twgljs.org/docs/module-twgl.html#.addExtensionsToContext). Как вы и упомянули, вместо - - ext = gl.getExtension('ANGLE_instanced_arrays'); - ext.drawArraysInstancedANGLE(...) - -Вы делаете - - twgl.addExtensionsToContext(gl); - gl.drawArraysInstanced(...); - -Я не думаю, что будет заметная разница в производительности. Эти функции вызываются всего несколько сотен раз за кадр, и обёртка не станет узким местом. - -Но суть не в том, чтобы поддерживать WebGL1 и WebGL2 одновременно. Скорее, чтобы писать код одинаково для обоих API. - -Тем не менее, есть реальные различия между API. Например, чтобы использовать FLOAT RGBA текстуру в WebGL1: - - gl.texImage2D(target, level, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, ...) - -В WebGL2: - - gl.texImage2D(target, level, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, ...) - -WebGL2 выдаст ошибку, если вызвать его так же, как WebGL1. [Есть и другие отличия](https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html). - -Впрочем, вот такой вызов будет работать и в WebGL1, и в WebGL2. Спецификация прямо говорит, что такая комбинация даёт RGBA8 в WebGL2. - -Обратите внимание, что ваш пример с RGBA8 не совсем верен. - - gl.texImage2D(target, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, ...) - -Главное отличие — нет смысла использовать WebGL2, если вам хватает WebGL1. Или наоборот: если вам нужен WebGL2, то откатиться на WebGL1 будет сложно. - -Вы упомянули define-ы для шейдеров, но что делать с фичами WebGL2, которых нет в WebGL1? Например, `textureFetch`, оператор `%`, integer attributes и т.д. Если они нужны — придётся писать отдельный шейдер только для WebGL2. Если не нужны — зачем тогда WebGL2? - -Конечно, если хочется, можно сделать более продвинутый рендерер для WebGL2 и попроще для WebGL1. - -TL;DR: На мой взгляд, выберите что-то одно. - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - user2503048 - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md b/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md deleted file mode 100644 index dc9262c73..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md +++ /dev/null @@ -1,81 +0,0 @@ -Title: Как определить, есть ли у изображения альфа-канал -Description: Как определить, есть ли у изображения альфа-канал -TOC: Как определить, есть ли у изображения альфа-канал - -## Вопрос: - -Когда вы загружаете текстуру WebGL напрямую из DOM-изображения, как узнать, есть ли у изображения альфа-канал? Можно ли это определить, кроме как угадывать по имени файла (например, если есть .PNG — возможно RGBA, иначе RGB)? В DOM-изображении есть ширина и высота, но я не вижу ничего, что бы говорило о формате. Например: - - const img = await loadDOMImage(url); - const format = gl.RGBA; //Это всегда должно быть RGBA? Я трачу память, если там только RGB - const internalFormat = gl.RGBA; - const type = gl.UNSIGNED_BYTE; //Это точно правильно? DOM не поддерживает HDR форматы? - gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, img); - -Моя функция загрузки выглядит так: - - async loadDOMImage(url) { - return new Promise( - (resolve, reject)=>{ - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.addEventListener('load', function() { - resolve(img); - }, false); - img.addEventListener('error', function(err) { - reject(err); - }, false); - - img.src = uri; - - } - ); - } - - - - - -## Ответ: - -> как узнать, есть ли у изображения альфа-канал? - -Никак. Можно только догадываться. - -1. Можно посмотреть, заканчивается ли URL на .png, и предположить, что там есть альфа. Можно ошибиться. - -2. Можно нарисовать изображение в 2D canvas, затем вызвать getImageData, прочитать все альфа-значения и посмотреть, есть ли среди них не 255. - -> ``` -> const format = gl.RGBA; -> ``` -> Это всегда должно быть RGBA? Я трачу память, если там только RGB - -Скорее всего, память не тратится зря. Большинство GPU лучше работают с RGBA, так что даже если вы выберете RGB, это вряд ли сэкономит память. - -> ``` -> const type = gl.UNSIGNED_BYTE; -> ``` -> Это точно правильно? - -`texImage2D` берёт изображение, которое вы передаёте, и конвертирует его в `type` и `format`. Затем эти данные передаются на GPU. - -> DOM не поддерживает HDR форматы? - -Это не определено и зависит от браузера. Мне не известны HDR форматы, поддерживаемые браузерами. Какие форматы поддерживает браузер — решает сам браузер. - ---- - -Частый вопрос: нужно ли включать смешивание/прозрачность для конкретной текстуры. Некоторые ошибочно думают, что если у текстуры есть альфа, то надо, иначе — нет. Это неверно. Нужно ли использовать смешивание/прозрачность — это отдельный вопрос, не связанный с форматом изображения, и это нужно хранить отдельно. - -То же самое касается других вещей, связанных с изображениями. В каком формате загружать изображение в GPU — это зависит от приложения и задачи, и не связано с форматом самого файла изображения. - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - griffin2000 - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md b/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md deleted file mode 100644 index 0ec43ca64..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md +++ /dev/null @@ -1,88 +0,0 @@ -Title: Как использовать прозрачность 2D-спрайта как маску -Description: Как использовать прозрачность 2D-спрайта как маску -TOC: Как использовать прозрачность 2D-спрайта как маску - -## Вопрос: - - ```javascript -if (statuseffect) { - // Очистка stencil buffer - gl.clearStencil(0); - gl.clear(gl.STENCIL_BUFFER_BIT); - - - gl.stencilFunc(gl.ALWAYS, 1, 1); - gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE); - - - gl.colorMask(false, false, false, false); - - gl.enable(gl.STENCIL_TEST); - - // Рисуем маску через gl.drawArrays L111 - drawImage(statuseffectmask.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4) - - // Говорим stencil теперь рисовать/оставлять только пиксели, равные 1 — что мы установили ранее - gl.stencilFunc(gl.EQUAL, 1, 1); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); - - // возвращаем обратно цветовой буфер - gl.colorMask(true, true, true, true); - - - drawImage(statuseffect.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4) - - - gl.disable(gl.STENCIL_TEST); - } - - -``` -Я пытаюсь добиться такого эффекта [![enter image description here][1]][1] -Где берётся прозрачность спрайта, и затем другой спрайт рисуется только в областях, где нет прозрачности. Спасибо. - -[1]: https://i.stack.imgur.com/ESdGp.png - -## Ответ: - -Не совсем понятно, зачем вам использовать stencil для этого. Обычно вы бы просто [настроили смешивание (blending) и использовали прозрачность для смешивания](https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html). - -Если вы действительно хотите использовать stencil, вам нужно сделать шейдер, который вызывает `discard`, если прозрачность (alpha) меньше определённого значения, чтобы stencil устанавливался только там, где спрайт не прозрачный: - -``` -precision highp float; - -varying vec2 v_texcoord; - -uniform sampler2D u_texture; -uniform float u_alphaTest; - -void main() { - vec4 color = texture2D(u_texture, v_texcoord); - if (color.a < u_alphaTest) { - discard; // не рисовать этот пиксель - } - gl_FragColor = color; -} -``` - -Но дело в том, что это уже нарисует текстуру с прозрачностью и без использования stencil. - -{{{example url="../webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask-example-1.html"}}} - -Если же вы действительно хотите использовать stencil, теперь, когда код отбрасывает некоторые пиксели, всё должно работать, и ваш код был верным. Обратите внимание, что код ниже не очищает stencil, потому что он по умолчанию очищается каждый кадр - -{{{example url="../webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask-example-2.html"}}} - -Также отмечу, что это, вероятно, лучше делать с помощью альфа-смешивания, передавая обе текстуры в один шейдер и передавая ещё одну матрицу или другие uniforms, чтобы применить альфу одной текстуры к другой. Это будет более гибко, так как можно смешивать по всем значениям от 0 до 1, тогда как с помощью stencil можно только маскировать 0 или 1. - -Я не говорю "не используйте stencil", а лишь отмечаю, что бывают ситуации, когда это лучший вариант, и когда — нет. Только вы можете решить, какой способ лучше для вашей задачи. - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - Evan Wrynn - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md b/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md deleted file mode 100644 index 09ca2727e..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md +++ /dev/null @@ -1,183 +0,0 @@ -Title: Как использовать текстуры как данные -Description: Как использовать текстуры как данные -TOC: Как использовать текстуры как данные - -## Вопрос: - -Я изучал уроки по WebGL, такие как [webglfundamentals](https://webglfundamentals.org/), и столкнулся с проблемой — мне кажется, что мне нужно использовать текстуру, которую я создам, чтобы передавать информацию напрямую во фрагментный шейдер, но у меня не получается правильно индексировать текстуру. - -Цель — передать информацию об источниках света (местоположение и цвет), которая будет учитываться при расчёте цвета фрагмента. В идеале эта информация должна быть динамической как по значению, так и по длине. - -## Воспроизведение -Я создал упрощённую версию проблемы в этом fiddle: [WebGL - Data Texture Testing](https://jsfiddle.net/oclyke/muf0deoL/86/) - -Вот часть кода. - -В **одноразовой инициализации** мы создаём текстуру, заполняем её данными и применяем, как кажется, самые надёжные настройки (без mips, без проблем с упаковкой байтов[?]) -``` - // lookup uniforms - var textureLocation = gl.getUniformLocation(program, "u_texture"); - - // Create a texture. - var texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - - // fill texture with 1x3 pixels - const level = 0; - const internalFormat = gl.RGBA; // Я также пробовал gl.LUMINANCE - // но это сложнее отлаживать - const width = 1; - const height = 3; - const border = 0; - const type = gl.UNSIGNED_BYTE; - const data = new Uint8Array([ - // R, G, B, A (не используется) // : индекс 'texel' (?) - 64, 0, 0, 0, // : 0 - 0, 128, 0, 0, // : 1 - 0, 0, 255, 0, // : 2 - ]); - const alignment = 1; // для этой текстуры не обязательно, но - gl.pixelStorei(gl.UNPACK_ALIGNMENT, alignment); // думаю, это не мешает - gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, - internalFormat, type, data); - - // set the filtering so we don't need mips and it's not filtered - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); -``` - -В **отрисовке** (которая происходит только один раз, но теоретически может повторяться) мы явно указываем программе использовать нашу текстуру -``` - // Сказать шейдеру использовать текстурный юнит 0 для u_texture - gl.activeTexture(gl.TEXTURE0); // добавил эту и следующую строку для уверенности... - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.uniform1i(textureLocation, 0); -``` - -Наконец, во фрагментном шейдере мы просто пытаемся надёжно использовать один 'texel' для передачи информации. У меня не получается понять, как надёжно получить значения, которые я сохранил в текстуре. -``` -precision mediump float; - -// Текстура. -uniform sampler2D u_texture; - -void main() { - - vec4 sample_00 = texture2D(u_texture, vec2(0, 0)); - // Этот сэмпл обычно правильный. - // Изменение данных для B-канала texel - // индекса 0, как ожидается, добавляет синий - - vec4 sample_01 = texture2D(u_texture, vec2(0, 1)); - vec4 sample_02 = texture2D(u_texture, vec2(0, 2)); - // Эти сэмплы, как я ожидал, должны работать, так как - // ширина текстуры установлена в 1 - // Почему-то 01 и 02 показывают один и тот же цвет - - vec4 sample_10 = texture2D(u_texture, vec2(1, 0)); - vec4 sample_20 = texture2D(u_texture, vec2(2, 0)); - // Эти сэмплы просто для теста — не думаю, - // что они должны работать - - // выбираем, какой сэмпл показать - vec4 sample = sample_00; - gl_FragColor = vec4(sample.x, sample.y, sample.z, 1); -} -``` - -## Вопрос(ы) - -Является ли использование текстуры лучшим способом для этого? Я слышал, что можно передавать массивы векторов, но текстуры, кажется, более распространены. - -Как правильно создавать текстуру? (особенно когда я указываю 'width' и 'height', я должен иметь в виду размеры texel или количество элементов gl.UNSIGNED_BYTE, которые я буду использовать для создания текстуры?? [документация texImage2D](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml)) - -Как правильно индексировать текстуру во фрагментном шейдере, если не использовать 'varying' типы? (т.е. я просто хочу получить значение одного или нескольких конкретных texel — без интерполяции [почти не связано с вершинами]) - -### Другие ресурсы -Я прочитал всё, что мог на эту тему. Вот не полный список: - -* JMI Madison утверждает, что разобрался, но [решение](https://stackoverflow.com/questions/34873832/webgl-fragment-shader-pass-array) утопает в проектном коде -* [webglfundamentals](https://webglfundamentals.org/) почти подходит — [пример с 3x2 data texture](https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html) — но там используется интерполяция и это не совсем мой случай -* Вот кто-то обсуждает [попытку использовать массивы vec3](https://community.khronos.org/t/passing-array-of-vec3-to-fragment-shader/74450) -* И конечно я сравнивал с документацией OpenGL ([texImage2D](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml) и [texture2d](https://thebookofshaders.com/glossary/?search=texture2D)) - -**Edit** Вот ещё ресурс: [Hassles with array access in WebGL, and a couple of workarounds](https://www.john-smith.me/hassles-with-array-access-in-webgl-and-a-couple-of-workarounds.html). Обнадёживает. - -Это меня очень беспокоит. - -Спасибо заранее! - -## Ответ: - -Обращение к отдельным пикселям текстуры в WebGL1 использует такую формулу - -``` -vec2 pixelCoord = vec2(x, y); -vec2 textureDimension = vec2(textureWidth, textureHeight) -vec2 texcoord = (pixelCoord + 0.5) / textureDimensions; -vec4 pixelValue = texture2D(someSamplerUniform, texcoord); -``` - -Потому что текстурные координаты считаются по краям. Если у вас текстура 2x1 - -``` -1.0+-------+-------+ - | | | - | A | B | - | | | -0.0+-------+-------+ - 0.0 0.5 1.0 -``` - -Текстурная координата в центре пикселя A = 0.25, 0.5. В центре пикселя B = 0.75, 0.5 - -Если не использовать формулу выше, а просто pixelCoord / textureDimensions, то вы попадёте между пикселями, и из-за ошибок округления получите не тот пиксель. - -Конечно, если вы используете текстуры для данных, вам также нужно установить фильтрацию `gl.NEAREST`. - -В WebGL2 можно просто использовать `texelFetch` - -``` -ivec2 pixelCoord = ivec2(x, y); -int mipLevel = 0; -vec4 pixelValue = texelFetch(someSamplerUniform, texcoord, mipLevel); -``` - -Рабочий пример использования текстур для данных: [здесь](https://webglfundamentals.org/webgl/lessons/webgl-pulling-vertices.html) - -> Является ли использование текстуры лучшим способом для этого? Я слышал, что можно передавать массивы векторов, но текстуры, кажется, более распространены. - -Для чего? Не совсем понятно, что вы хотите сделать. Каждый пиксель будет иметь свой источник света? - -> Как правильно создавать текстуру? (особенно когда я указываю 'width' и 'height', я должен иметь в виду размеры texel или количество элементов gl.UNSIGNED_BYTE, которые я буду использовать для создания текстуры?? документация texImage2D) - -Делайте так, как проще или как требуется. Например, если у вас 5 данных на объект, я бы поместил каждое значение на отдельную строку текстуры. Тогда можно сделать - -``` -vec4 datum1 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY0)); -vec4 datum2 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY1)); -vec4 datum3 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY2)); -vec4 datum4 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY3)); -``` - -Где indexTexCoordX и rowTexCoordY0-3 вычисляются по формуле выше. rowTexCoordY0-3 могут быть даже константами. - -У текстур есть ограничения по размеру, так что если данных больше, чем помещается в одном измерении, придётся упаковывать плотнее и делать больше вычислений для извлечения. - -Имейте в виду, что у текстур есть кэш, поэтому желательно, чтобы данные, которые вы извлекаете, были рядом с теми, что вы извлекали до этого. Если вы каждый раз прыгаете по текстуре за следующим значением, производительность упадёт (хотя всё равно может быть быстрее других решений, в зависимости от задачи). - -> Как правильно индексировать текстуру во фрагментном шейдере, если не использовать 'varying' типы? (т.е. я просто хочу получить значение одного или нескольких конкретных texel — без интерполяции [почти не связано с вершинами]) - -Единственные изменяемые входы во фрагментный шейдер — varyings, `gl_FragCoord` (координата пикселя, в который пишется) и `gl_PointCoord`, доступен только при рисовании `POINTS`. Так что нужно использовать один из них, иначе все остальные значения будут одинаковы для всех пикселей. - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - oclyke - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md b/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md deleted file mode 100644 index 6cebd840d..000000000 --- a/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md +++ /dev/null @@ -1,93 +0,0 @@ -Title: Как использовать stencil buffer -Description: Как использовать stencil buffer -TOC: Как использовать stencil buffer - -## Вопрос: - -Как я могу использовать stencil buffer для самой простой программы? -Я прочитал много разных тем об этом, но не нашел подробного руководства. -Я хочу вырезать отверстие на каждой стороне созданного тетраэдра. - -[![enter image description here][1]][1] - -Пожалуйста, объясните мне пошагово, как использовать stencil buffer? - -[Ссылка на мою программу][2] - - - [1]: https://i.stack.imgur.com/yV9oD.png - [2]: https://dropfiles.ru/filesgroup/62503e88028a16b1055f78a7e2b70456.html - -## Ответ: - -Чтобы использовать stencil buffer, вы должны сначала запросить его при создании контекста webgl - - const gl = someCanvasElement.getContext('webgl', {stencil: true}); - - -Затем включите тест трафарета (stencil test) - -``` - gl.enable(gl.STENCIL_TEST); -``` - -Настройте тест так, чтобы он всегда проходил, и установите опорное значение в 1 - -``` - gl.stencilFunc( - gl.ALWAYS, // тест - 1, // опорное значение - 0xFF, // маска - ); -``` - -И задайте операцию так, чтобы мы устанавливали stencil в опорное значение, когда оба теста (stencil и depth) проходят - -``` - gl.stencilOp( - gl.KEEP, // что делать, если stencil тест не прошёл - gl.KEEP, // что делать, если depth тест не прошёл - gl.REPLACE, // что делать, если оба теста прошли - ); -``` - -Теперь рисуем первый внутренний треугольник - -``` -... много настроек для одного треугольника ... - -gl.drawArrays(...) или gl.drawElements(...) -``` - -Затем меняем тест так, чтобы он проходил только если stencil равен нулю - -``` - gl.stencilFunc( - gl.EQUAL, // тест - 0, // опорное значение - 0xFF, // маска - ); - gl.stencilOp( - gl.KEEP, // что делать, если stencil тест не прошёл - gl.KEEP, // что делать, если depth тест не прошёл - gl.KEEP, // что делать, если оба теста прошли - ); - -``` - -и теперь мы можем нарисовать что-то ещё (больший треугольник), и оно будет рисоваться только там, где в stencil buffer стоит 0, то есть везде, кроме области, где был нарисован первый треугольник. - -Пример: - -{{{example url="../webgl-qna-how-to-use-the-stencil-buffer-example-1.html"}}} - - - -
-
Вопрос и процитированные части являются - CC BY-SA 4.0 от - AnatoliyC - из - здесь -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md b/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md deleted file mode 100644 index 67aa8b6c4..000000000 --- a/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md +++ /dev/null @@ -1,43 +0,0 @@ -Title: Ошибка invalid type при вызове readPixels -Description: Ошибка invalid type при вызове readPixels -TOC: Ошибка invalid type при вызове readPixels - -## Вопрос: - - context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.FLOAT, pixels); - -Вот этот код. В консоли появляется ошибка: -**WebGL: INVALID_ENUM: readPixels: invalid type** - -Но вот так всё работает нормально: - - context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.UNSIGNED_BYTE, pixels); - -Float или int вроде как должны поддерживаться, но работает только unsigned_byte. -В интернете нет ресурсов, как правильно применить тип, который, кажется, должен работать. -Везде разные примеры. - -## Ответ: - -FLOAT не гарантирован к поддержке. Единственная комбинация формат/тип, которая гарантированно поддерживается — RGBA/UNSIGNED_BYTE. [См. спецификацию, раздел 4.3.1](https://www.khronos.org/registry/OpenGL/specs/es/2.0/es_full_spec_2.0.pdf) - -Кроме этого, одна другая **зависящая от реализации** комбинация формат/тип может поддерживаться в зависимости от того, что вы читаете. Её можно узнать так: - -``` -const altFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); -const altType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); -``` - -{{{example url="../webgl-qna-i-get-invalid-type-error-when-calling-readpixels-example-1.html"}}} - -Код выше создаёт текстуру RGBA/FLOAT, прикрепляет её к framebuffer, а затем проверяет альтернативную комбинацию формат/тип для чтения. В Chrome это RGBA/UNSIGNED_BYTE, в Firefox — RGBA/FLOAT. Оба варианта валидны, так как альтернативная комбинация **зависит от реализации**. - - - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - Tony Arntsen - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md b/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md deleted file mode 100644 index f51c169f5..000000000 --- a/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md +++ /dev/null @@ -1,235 +0,0 @@ -Title: Показать ночной вид против дневного вида на 3D сфере Земли -Description: Показать ночной вид против дневного вида на 3D сфере Земли -TOC: Показать ночной вид против дневного вида на 3D сфере Земли - -## Вопрос: - -Я использую Three.js как фреймворк для разработки космического симулятора и пытаюсь, но не могу заставить работать ночные огни. - -Симулятор доступен здесь: - -[orbitingeden.com][1] - -и страница, запускающая фрагмент кода ниже, находится здесь: - -[orbitingeden.com/orrery/soloearth.html][2] - -Код для примера страницы здесь. Я даже не знаю, с чего начать. Я пытался рендерить два глобуса на несколько единиц друг от друга, один ближе к солнцу (дневная версия) и один дальше (ночная версия), но есть много проблем, не в последнюю очередь то, что они начинают перекрывать друг друга странными додекаэдрическими способами. Я принял идею tDiffuse2 из этого [оррери][3], но не смог заставить её работать. - - - - - three.js webgl - earth - - - - - - - - - - - [1]: http://orbitingeden.com - [2]: http://orbitingeden.com/orrery/soloearth.html - [3]: http://www.esfandiarmaghsoudi.com/Apps/SolarSystem/ - -## Ответ: - -Если я правильно понимаю ваш вопрос.... - -Я не знаю three.js, но в общем я бы сделал это, имея шейдер, которому передаются и дневная, и ночная текстуры, а затем выбирая одну или другую в шейдере. Например: - - uniform sampler2D dayTexture; - uniform sampler2D nightTexture; - varying vec3 v_surfaceToLight; // assumes this gets passed in from vertex shader - varying vec4 v_normal; // assumes this gets passed in from vertex shader - varying vec2 v_texCoord; // assumes this gets passed in from vertex shader - - void main () { - vec3 normal = normalize(v_normal); - vec3 surfaceToLight = normalize(v_surfaceToLight); - float angle = dot(normal, surfaceToLight); - vec4 dayColor = texture2D(dayTexture, v_texCoords); - vec4 nightColor = texture2D(nightTexture, v_texCoord); - vec4 color = angle < 0.0 ? dayColor : nightColor; - - ... - - gl_FragColor = color * ...; - } - -В основном вы берёте расчёт освещения и вместо использования его для освещения используете его для выбора текстуры. Расчёт освещения обычно использует скалярное произведение между нормалью поверхности и направлением света (солнца) от поверхности. Это даёт вам косинус угла между этими двумя векторами. Косинус идёт от -1 до 1, так что если значение от -1 до 0, это обращено от солнца, если от 0 до +1, это обращено к солнцу. - -Строка - - vec4 color = angle < 0.0 ? dayColor : nightColor; - -выбирает день или ночь. Это будет резкий переход. Вы можете поэкспериментировать с чем-то более размытым, например: - - - // convert from -1 <-> +1 to 0 <-> +1 - float lerp0To1 = angle * 0.5 + 0.5; - - // mix between night and day - vec4 color = mix(nightColor, dayColor, lerp0to1); - - -Это даст вам 100% день в точке, прямо обращённой к солнцу, и 100% ночь в точке, прямо противоположной солнцу, и смесь между ними. Вероятно, не то, что вы хотите, но вы можете поиграть с числами. Например: - - // sharpen the mix - angle = clamp(angle * 10.0, -1.0, 1.0); - - // convert from -1 <-> +1 to 0 <-> +1 - float lerp0To1 = angle * 0.5 + 0.5; - - // mix between night and day - vec4 color = mix(nightColor, dayColor, lerp0to1); - - -Надеюсь, это имело смысл. - ----- - -Так что я потратил немного времени на создание примера Three.js, отчасти чтобы изучить Three.js. Пример здесь. - -{{{example url="../webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere-example-1.html"}}} - -Шейдер, который я использовал, это: - - uniform sampler2D dayTexture; - uniform sampler2D nightTexture; - - uniform vec3 sunDirection; - - varying vec2 vUv; - varying vec3 vNormal; - - void main( void ) { - vec3 dayColor = texture2D( dayTexture, vUv ).rgb; - vec3 nightColor = texture2D( nightTexture, vUv ).rgb; - - // compute cosine sun to normal so -1 is away from sun and +1 is toward sun. - float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection); - - // sharpen the edge beween the transition - cosineAngleSunToNormal = clamp( cosineAngleSunToNormal * 10.0, -1.0, 1.0); - - // convert to 0 to 1 for mixing - float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5; - - // Select day or night texture based on mixAmount. - vec3 color = mix( nightColor, dayColor, mixAmount ); - - gl_FragColor = vec4( color, 1.0 ); - - // comment in the next line to see the mixAmount - //gl_FragColor = vec4( mixAmount, mixAmount, mixAmount, 1.0 ); - } - -Большая разница от предыдущего в том, что поскольку солнце обычно считается направленным источником света, поскольку оно так далеко, то всё, что вам нужно — это его направление. Другими словами, в какую сторону оно указывает относительно земли. - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - Orbiting Eden - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md b/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md deleted file mode 100644 index dd68944cb..000000000 --- a/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md +++ /dev/null @@ -1,76 +0,0 @@ -Title: Самый быстрый способ нарисовать много кругов -Description: Самый быстрый способ нарисовать много кругов -TOC: Самый быстрый способ нарисовать много кругов - -## Вопрос: - -Сейчас я рисую тысячи кругов, инстанцируя [геометрию круга](https://threejs.org/docs/#api/en/geometries/CircleBufferGeometry) (много треугольников). - -[![enter image description here][1]][1] - -Альтернативно, я мог бы просто инстанцировать quad (2 треугольника), а круг "вырезать" во фрагментном шейдере с помощью distance-функции и `discard`. - -[![enter image description here][2]][2] - -Какой подход будет быстрее? — дороже ли рисовать много треугольников, чем вычисления во фрагментном шейдере? - - [1]: https://i.stack.imgur.com/CZhOa.png - [2]: https://i.stack.imgur.com/BG4p5.png - -## Ответ: - -Самый быстрый способ может зависеть от GPU и множества других факторов: как вы рисуете круги (2D, 3D), используете ли смешивание (blending), z-buffer и т.д. Но в целом — меньше треугольников быстрее, чем больше, и меньше пикселей быстрее, чем больше. Так что... всё, что мы можем — это попробовать. - -Сначала просто нарисуем текстурированные квадраты без blending. У меня на GPU в этом 300x150 canvas с инстансингом получается 20-30 тысяч quad'ов на 60fps. - -{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-1.html"}}} - -И такая же производительность при 60fps, если использовать повторяющуюся геометрию вместо инстансинга. Это удивительно, потому что 7-8 лет назад повторяющаяся геометрия была на 20-30% быстрее. Почему — не знаю: лучше GPU, драйвер или что-то ещё. - -{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-2.html"}}} - -Дальше — текстуры или вычисление круга во фрагментном шейдере. - -{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-3.html"}}} - -Разницы не заметил. Пробую вашу функцию круга: - -{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-4.html"}}} - -Опять разницы не заметил. Замечу: как уже говорил, результаты WebGL очень нестабильны. Первый тест — 28k на 60fps, второй — 23k. Ожидал, что второй будет быстрее, но потом снова первый — 23k, последний — 29k, потом предыдущий — 29k. Короче, тестировать производительность в WebGL почти невозможно. Слишком много факторов, всё многопроцессное, получить стабильные результаты невозможно. - -Можно попробовать discard: - -{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-5.html"}}} - -Судя по ощущениям, discard медленнее. Насколько помню, discard медленный, потому что без него GPU знает заранее, что обновит z-buffer, а с discard — только после выполнения шейдера, и это мешает оптимизациям. - -На этом остановлюсь, потому что слишком много комбинаций для тестов. - -Можно попробовать включить blending. Blending обычно медленнее, потому что нужно смешивать (читать фон), но медленнее ли, чем discard — не знаю. - -Включён ли depth test? Если да, порядок отрисовки важен. - -Можно попробовать не-quads, а, например, шестиугольники или восьмиугольники — тогда меньше пикселей попадёт во фрагментный шейдер. Думаю, разницу увидите только на больших кругах. Например, quad 100x100 — это 10k пикселей, идеальный круг — pi*r^2 ≈ 7853, то есть на 21% меньше. Шестиугольник — ~8740 пикселей, на 11% меньше. Восьмиугольник — где-то между. Рисовать на 11-21% меньше пикселей — обычно плюс, но для шестиугольника будет в 3 раза больше треугольников, для восьмиугольника — в 4 раза. Всё надо тестировать. - -Это ещё раз показывает, что для больших кругов на большом canvas относительные результаты будут другими: больше пикселей на круг, больше времени на пиксели, меньше — на вершины и/или переключения GPU. - -## Обновление - -Тесты в Chrome и Firefox: в Chrome на той же машине 60-66k во всех случаях. Почему разница такая большая — не знаю, ведь WebGL почти ничего не делает. Все 4 теста — по одному draw call на кадр. Но, по крайней мере, на 2019-10 Chrome в этом случае в 2 раза быстрее Firefox. - -Есть идея: у меня ноутбук с двумя GPU. При создании контекста можно указать, что вы хотите, через атрибут `powerPreference`: - - const gl = document.createContext('webgl', { - powerPreference: 'high-performance', - }); - -Варианты: 'default', 'low-power', 'high-performance'. 'default' — "пусть браузер решает", но в итоге всё равно решает браузер. В любом случае, у меня в Firefox это ничего не меняет. - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - kindoflike - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md b/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md deleted file mode 100644 index 2bae37212..000000000 --- a/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md +++ /dev/null @@ -1,42 +0,0 @@ -Title: WebGL 2D тайлмапы -Description: WebGL 2D тайлмапы -TOC: WebGL 2D тайлмапы - -## Вопрос: - -Я создаю простую 2D веб-игру с типичной тайл-картой и спрайтами. - -Особенность в том, что мне нужны плавные элементы управления камерой — как перемещение, так и масштабирование (зум). - -Я пробовал и Canvas 2D API, и WebGL, но в обоих случаях не могу избежать артефактов сетки (bleeding grid line artifacts), при этом поддерживая правильное масштабирование. - -Если это важно: все мои тайлы размером 1, масштабируются до нужного размера, все координаты — целые числа, и я использую текстуру-атлас. - -Вот пример картинки с моим WebGL-кодом, где тонкие красные/белые линии нежелательны: -[![enter image description here][1]][1] - - [1]: https://i.stack.imgur.com/ZziiR.png - -Я помню, как писал спрайтовые тайлмапы годами назад на desktop GL, иронично используя похожий код (более или менее эквивалентный тому, что можно сделать с WebGL 2), и там никогда не было таких проблем. - -Сейчас думаю попробовать DOM-элементы, но боюсь, что это не будет выглядеть или работать плавно. - -## Ответ: - -Одно из решений — рисовать тайлы во фрагментном шейдере. - -У вас есть карта, скажем, `Uint32Array`. Разбиваете её на единицы по 4 байта каждая. Первые 2 байта — ID тайла, последний байт — флаги. - -Когда вы проходите по quad'у для каждого пикселя, вы смотрите в текстуру тайлмапы, какой это тайл, затем используете это для вычисления UV-координат, чтобы получить пиксели этого тайла из текстуры тайлов. Если у вашей текстуры тайлов установлена выборка gl.NEAREST, то bleeding никогда не будет. - -Заметьте, что в отличие от традиционных тайлмапов, ID каждого тайла — это X,Y-координата тайла в текстуре тайлов. Другими словами, если ваша текстура тайлов имеет 16x8 тайлов, и вы хотите показать тайл на позиции 7 по горизонтали и 4 по вертикали, то ID этого тайла — 7,4 (первый байт 7, второй байт 4), тогда как в традиционной CPU-системе ID тайла был бы, вероятно, 4*16+7 или 71 (71-й тайл). Можно добавить код в шейдер для более традиционной индексации, но поскольку шейдер всё равно должен конвертировать ID в 2D-координаты текстуры, проще использовать 2D-ID. - -{{{example url="../webgl-qna-webgl-2d-tilemaps-example-1.html"}}} - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - user2503048 - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md b/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md deleted file mode 100644 index 4e2c9dcef..000000000 --- a/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md +++ /dev/null @@ -1,90 +0,0 @@ -Title: Эффект Дросте в WebGL -Description: Эффект Дросте в WebGL -TOC: Эффект Дросте в WebGL - -## Вопрос: - -Я пытаюсь использовать WebGL для создания [эффекта Дросте](https://en.wikipedia.org/wiki/Droste_effect) на гранях куба. В viewport'е один меш — куб, и все его грани используют одну текстуру. Для создания эффекта Дросте я обновляю текстуру на каждом кадре, делая снимок `canvas`, в WebGL-контекст которого рисую, что со временем даёт эффект Дросте, так как снимок содержит всё больше и больше вложенных прошлых кадров. - -Демо того, что у меня сейчас работает, здесь: - -https://tomashubelbauer.github.io/webgl-op-1/?cubeTextured - -Код выглядит так: - -``` -// Set up fragment and vertex shader and attach them to a program, link the program -// Create a vertex buffer, an index buffer and a texture coordinate buffer -// Tesselate the cube's vertices and fill in the index and texture coordinate buffers -const textureCanvas = document.createElement('canvas'); -textureCanvas.width = 256; -textureCanvas.height = 256; -const textureContext = textureCanvas.getContext('2d'); - -// In every `requestAnimationFrame`: -textureContext.drawImage(context.canvas, 0, 0); -const texture = context.createTexture(); -context.bindTexture(context.TEXTURE_2D, texture); -context.texImage2D(context.TEXTURE_2D, 0, context.RGBA, context.RGBA, context.UNSIGNED_BYTE, textureCanvas); -context.generateMipmap(context.TEXTURE_2D); -// Clear the viewport completely (depth and color buffers) -// Set up attribute and uniform values, the projection and model view matrices -context.activeTexture(context.TEXTURE0); -context.bindTexture(context.TEXTURE_2D, texture); -context.uniform1i(fragmentShaderTextureSamplerUniformLocation, 0); -context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0) -``` - -Это основная часть. Есть отдельный canvas от WebGL, на который рисуется WebGL canvas перед каждым WebGL кадром, и этот canvas используется для создания текстуры для данного кадра, которая применяется к граням куба согласно буферу координат текстуры и uniform'у сэмплера текстуры, переданному во фрагментный шейдер, который просто использует `gl_FragColor = texture2D(textureSampler, textureCoordinate)`, как и ожидается. - -Но это очень медленно (30 FPS на этом простом демо с одним кубом, тогда как все мои другие демо с порядком больше треугольников всё ещё держат 60 FPS `requestAnimationFrame`). - -Также странно делать это "вне" WebGL, используя внешний canvas, когда кажется, что это должно быть достижимо только с помощью WebGL. - -Я знаю, что WebGL держит два буфера — один для активного кадра и back buffer для недавно нарисованного кадра, и они меняются местами с каждым кадром для мгновенного обновления экрана. Можно ли получить доступ к этому back buffer и использовать его как текстуру? Можете ли вы предоставить пример кода, как это сделать? - -## Ответ: - -Из [этой статьи](https://webglfundamentals.org/webgl/lessons/webgl-render-to-texture.html) - -Обычный способ — рендерить в текстуру, прикрепив её к framebuffer. - -``` -const fb = gl.createFramebuffer(); -gl.bindFramebuffer(gl.FRAMEBUFFER, fb); -gl.framebufferTexture2D( - gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0 /* level */) -``` - -Теперь для рендеринга в текстуру: - -``` -gl.bindFramebuffer(gl.FRAMEBUFFER, fb); -gl.viewport(0, 0, textureWidth, textureHeight); -``` - -Для рендеринга в canvas: - -``` -gl.bindFramebuffer(gl.FRAMEBUFFER, null); -gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); -``` - -Для того, что вы хотите, нужны 2 текстуры, поскольку нельзя одновременно читать из и писать в одну текстуру, так что вы рисуете: - -* Рисуете изображение в TextureA -* Рисуете предыдущий кадр (TextureB) в TextureA -* Рисуете куб с TextureA в TextureB -* Рисуете TextureB в Canvas - -{{{example url="../webgl-qna-webgl-droste-effect-example-1.html"}}} - -Что касается canvas и его 2 буферов — нет, нельзя напрямую использовать их как текстуры. Можно вызвать `gl.copyTexImage2D` или `gl.copyTexSubImage2D` для копирования части canvas в текстуру, так что это другое решение. Оно менее гибкое и, думаю, медленнее, чем метод framebuffer. - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - Tomáš Hübelbauer - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md b/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md deleted file mode 100644 index d88c9508e..000000000 --- a/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md +++ /dev/null @@ -1,565 +0,0 @@ -Title: Что такое локальное начало координат 3D модели? -Description: Что такое локальное начало координат 3D модели? -TOC: Что такое локальное начало координат 3D модели? - -## Вопрос: - -В этом коде я не могу понять, почему вращение не происходит по круговой траектории. Это может быть базовая логика, но я не понимаю, почему траектория такая случайная. - -Я вращаю камеру по орбитальному движению, но она не следует за ним. - -Насколько я понимаю, я создал орбитальную камеру, и обратная к ней — это матрица вида. Значит, матрица вида будет трансформировать мировое пространство для этого результата. Есть ли ошибка в моём мышлении? - - - - - - "use strict"; - - const vertexShader = `#version 300 es - - in vec4 a_position; - in vec4 a_color; - - out vec4 v_color; - - uniform mat4 u_matrix; - - void main(){ - gl_Position = u_matrix*a_position; - v_color = a_color; - } - `; - - - const fragShader = `#version 300 es - - precision highp float; - - in vec4 v_color; - out vec4 frag_color; - - void main(){ - frag_color = v_color; - } - `; - - var cameraAngleDegree = 0; - var cameraAngle = 0; - const radius = 100; - var increment = 1; - var numFs = 5; - function main() { - - var canvas = document.querySelector("#canvas"); - var gl = canvas.getContext("webgl2"); - if (!gl) { - return; - } - requestAnimationFrame(function() { - init(gl); - }); - } - - function init(gl) { - - - const program = webglUtils.createProgramFromSources(gl, [vertexShader, fragShader]); - - const apositionLoc = gl.getAttribLocation(program, 'a_position'); - const acolorLoc = gl.getAttribLocation(program, 'a_color'); - const umatrixLoc = gl.getUniformLocation(program, 'u_matrix'); - - let vao = gl.createVertexArray(); - gl.bindVertexArray(vao); - - let positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - setGeometry(gl); - gl.enableVertexAttribArray(apositionLoc); - - let size = 3; - let type = gl.FLOAT; - let normalize = false; - let stride = 0; - let offset = 0; - gl.vertexAttribPointer(apositionLoc, size, type, normalize, stride, offset); - - let colorBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); - setColor(gl); - gl.enableVertexAttribArray(acolorLoc); - - size = 3; - type = gl.UNSIGNED_BYTE; - normalize = true; - stride = 0; - offset = 0; - gl.vertexAttribPointer(acolorLoc, size, type, normalize, stride, offset); - - let fov = degreeToRadian(60); - cameraAngle = degreeToRadian(cameraAngleDegree); - - function degreeToRadian(deg) { - return deg * Math.PI / 180; - } - - function radToDegree(rad) { - return rad * (180) / Math.PI; - } - - drawScene(); - - // webglLessonsUI.setupSlider("#cameraAngle", { value: radToDegree(cameraAngle), slide: updateCameraAngle, min: -360, max: 360 }); - - // function updateCameraAngle(event, ui) { - // cameraAngle = degreeToRadian(ui.value); - // drawScene(); - // } - - - function drawScene() { - - webglUtils.resizeCanvasToDisplaySize(gl.canvas); - - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - gl.enable(gl.CULL_FACE); - - gl.enable(gl.DEPTH_TEST); - - gl.useProgram(program); - - let aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; - - let projection = m4.perspective(fov, aspect, 1, 1000); - - const fPosition = [radius, 0, 0]; - - cameraAngleDegree += increment; - - cameraAngle =degreeToRadian(cameraAngleDegree); - - let camera = m4.yRotation(cameraAngle); - camera = m4.translate(camera, 0, 100, 300); - - let cameraPosition = [camera[12], camera[13], camera[14]]; - - // let up = [0, 1, 0]; - - // camera = m4.lookAt(cameraPosition, fPosition, up); - - let viewMatrix = m4.inverse(camera); - - let viewProjection = m4.multiply(projection, viewMatrix); - - for (var ii = 0; ii < numFs; ++ii) { - var angle = ii * Math.PI * 2 / numFs; - - var x = Math.cos(angle) * radius - 50; - var z = Math.sin(angle) * radius - 15; - var matrix = m4.translate(viewProjection, x, 0, z); - - // Set the matrix. - gl.uniformMatrix4fv(umatrixLoc, false, matrix); - - // Draw the geometry. - var primitiveType = gl.TRIANGLES; - var offset = 0; - var count = 16 * 6; - gl.drawArrays(primitiveType, offset, count); - } - // gl.uniformMatrix4fv(umatrixLoc, false, viewProjection); - - // var primitives = gl.TRIANGLES; - // var count = 16 * 6; - // var offset = 0; - // gl.drawArrays(primitives, offset, count); - - // } - - requestAnimationFrame(function() { - init(gl) - }); - - } - } - - function setGeometry(gl) { - - let positions = new Float32Array([ - - 0, 0, 0, - 0, 150, 0, - 30, 0, 0, - 0, 150, 0, - 30, 150, 0, - 30, 0, 0, - - // top rung front - 30, 0, 0, - 30, 30, 0, - 100, 0, 0, - 30, 30, 0, - 100, 30, 0, - 100, 0, 0, - - // middle rung front - 30, 60, 0, - 30, 90, 0, - 67, 60, 0, - 30, 90, 0, - 67, 90, 0, - 67, 60, 0, - - // left column back - 0, 0, 30, - 30, 0, 30, - 0, 150, 30, - 0, 150, 30, - 30, 0, 30, - 30, 150, 30, - - // top rung back - 30, 0, 30, - 100, 0, 30, - 30, 30, 30, - 30, 30, 30, - 100, 0, 30, - 100, 30, 30, - - // middle rung back - 30, 60, 30, - 67, 60, 30, - 30, 90, 30, - 30, 90, 30, - 67, 60, 30, - 67, 90, 30, - - // top - 0, 0, 0, - 100, 0, 0, - 100, 0, 30, - 0, 0, 0, - 100, 0, 30, - 0, 0, 30, - - // top rung right - 100, 0, 0, - 100, 30, 0, - 100, 30, 30, - 100, 0, 0, - 100, 30, 30, - 100, 0, 30, - - // under top rung - 30, 30, 0, - 30, 30, 30, - 100, 30, 30, - 30, 30, 0, - 100, 30, 30, - 100, 30, 0, - - // between top rung and middle - 30, 30, 0, - 30, 60, 30, - 30, 30, 30, - 30, 30, 0, - 30, 60, 0, - 30, 60, 30, - - // top of middle rung - 30, 60, 0, - 67, 60, 30, - 30, 60, 30, - 30, 60, 0, - 67, 60, 0, - 67, 60, 30, - - // right of middle rung - 67, 60, 0, - 67, 90, 30, - 67, 60, 30, - 67, 60, 0, - 67, 90, 0, - 67, 90, 30, - - // bottom of middle rung. - 30, 90, 0, - 30, 90, 30, - 67, 90, 30, - 30, 90, 0, - 67, 90, 30, - 67, 90, 0, - - // right of bottom - 30, 90, 0, - 30, 150, 30, - 30, 90, 30, - 30, 90, 0, - 30, 150, 0, - 30, 150, 30, - - // bottom - 0, 150, 0, - 0, 150, 30, - 30, 150, 30, - 0, 150, 0, - 30, 150, 30, - 30, 150, 0, - - // left side - 0, 0, 0, - 0, 0, 30, - 0, 150, 30, - 0, 0, 0, - 0, 150, 30, - 0, 150, 0, - - ]); - - gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW) - } - - function setColor(gl) { - gl.bufferData( - gl.ARRAY_BUFFER, - new Uint8Array([ - // left column front - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - - // top rung front - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - - // middle rung front - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - 200, 70, 120, - - // left column back - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - - // top rung back - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - - // middle rung back - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - 80, 70, 200, - - // top - 70, 200, 210, - 70, 200, 210, - 70, 200, 210, - 70, 200, 210, - 70, 200, 210, - 70, 200, 210, - - // top rung right - 200, 200, 70, - 200, 200, 70, - 200, 200, 70, - 200, 200, 70, - 200, 200, 70, - 200, 200, 70, - - // under top rung - 210, 100, 70, - 210, 100, 70, - 210, 100, 70, - 210, 100, 70, - 210, 100, 70, - 210, 100, 70, - - // between top rung and middle - 210, 160, 70, - 210, 160, 70, - 210, 160, 70, - 210, 160, 70, - 210, 160, 70, - 210, 160, 70, - - // top of middle rung - 70, 180, 210, - 70, 180, 210, - 70, 180, 210, - 70, 180, 210, - 70, 180, 210, - 70, 180, 210, - - // right of middle rung - 100, 70, 210, - 100, 70, 210, - 100, 70, 210, - 100, 70, 210, - 100, 70, 210, - 100, 70, 210, - - // bottom of middle rung. - 76, 210, 100, - 76, 210, 100, - 76, 210, 100, - 76, 210, 100, - 76, 210, 100, - 76, 210, 100, - - // right of bottom - 140, 210, 80, - 140, 210, 80, - 140, 210, 80, - 140, 210, 80, - 140, 210, 80, - 140, 210, 80, - - // bottom - 90, 130, 110, - 90, 130, 110, - 90, 130, 110, - 90, 130, 110, - 90, 130, 110, - 90, 130, 110, - - // left side - 160, 160, 220, - 160, 160, 220, - 160, 160, 220, - 160, 160, 220, - 160, 160, 220, - 160, 160, 220, - ]), - gl.STATIC_DRAW); - } - - main(); - - - - - - - - Traingle Webgl 2 - - - - - -
-
-
-
-
- - - - - - - - - - - - - - - -## Ответ: - -Если я правильно понимаю ваш вопрос, проблема в том, что кажется, будто камера то приближается к F, то удаляется от них. - -Проблема в том, что данные вершин для F построены так, что верхний левый передний угол находится в 0,0,0, оттуда они идут +X на 100 единиц (ширина 100 единиц), +Y на 150 единиц (высота 150 единиц), и +Z на 30 единиц (глубина 30 единиц). - -Поэтому когда вы рисуете их вокруг круга радиусом 100 единиц, их начало координат — это та часть, которую вы позиционируете, и получается это: - -[![enter image description here][1]][1] - -Изображение сверху, так что F — это просто прямоугольники. Зелёный круг — это локальное начало координат каждой F, её локальные 0,0,0. Другие вершины F относительно этого локального начала, поэтому они ближе к внешнему кругу (орбите камеры) с одной стороны и дальше с другой. - -Можно исправить, сдвинув F на -50 по X и -15 по Z. Другими словами: - -``` - var angle = ii * Math.PI * 2 / numFs; - - var x = Math.cos(angle) * radius - 50; - var z = Math.sin(angle) * radius - 15; -``` - -Что даёт такую ситуацию: - -[![enter image description here][2]][2] - -Локальное начало координат каждой F больше не на круге. - -Также можно исправить, центрировав данные вершин F — пройти по всем вершинам и вычесть 50 из X и 15 из Z. Это даст такую ситуацию: - -[![enter image description here][3]][3] - -Теперь начало координат каждой F центрировано, и её локальное начало на круге. - -Ещё один способ исправить — вычислить экстенты группы F, вычислить центр экстентов, переместить центр орбиты камеры туда, что даст такую ситуацию: - -[![enter image description here][4]][4] - - [1]: https://i.stack.imgur.com/bW9Xo.png - [2]: https://i.stack.imgur.com/CejYi.png - [3]: https://i.stack.imgur.com/MiQ6Z.png - [4]: https://i.stack.imgur.com/oysVQ.png - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - pravin poudel - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md b/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md deleted file mode 100644 index 10a1b388b..000000000 --- a/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md +++ /dev/null @@ -1,65 +0,0 @@ -Title: Когда выбирать highp, mediump, lowp в шейдерах -Description: Когда выбирать highp, mediump, lowp в шейдерах -TOC: Когда выбирать highp, mediump, lowp в шейдерах - -## Вопрос: - -Какие лучшие практики для них? Есть ли разница в производительности? - -## Ответ: - -> Какие лучшие практики для них? - -В основном это важно только на мобильных устройствах. Спецификация говорит, что реализация может всегда использовать более высокую точность, поэтому на десктопе и вершинный шейдер, и фрагментный шейдер всегда работают в highp. (Я не знаю десктопных GPU, для которых это не так) - -Из [спецификации](https://www.khronos.org/files/opengles_shading_language.pdf) раздел 4.5.2: - -> ## 4.5.2 Precision Qualifiers -> -> ... -> -> Precision qualifiers declare a minimum range and precision that the underlying implementation must use -> when storing these variables. Implementations may use greater range and precision than requested, but -> not less. - -Для мобильных устройств и планшетов есть несколько ответов. Нет лучшего. Это зависит от вас: - -1. используйте самую низкую точность, которую можете, но которая всё ещё делает то, что вам нужно. - -2. используйте highp и игнорируйте проблемы производительности и старые телефоны, где это не работает - -3. используйте mediump и игнорируйте баги (см. ниже) - -4. проверьте, поддерживает ли устройство пользователя highp, если нет — используйте разные шейдеры с меньшим количеством функций. - -WebGL по умолчанию использует highp для вершинных шейдеров, а фрагментные шейдеры не имеют значения по умолчанию, и вы должны указать одно. Более того, highp во фрагментном шейдере — это опциональная функция, и некоторые мобильные GPU её не поддерживают. Я не знаю, какой это процент в 2019 году. Насколько я знаю, большинство или даже все телефоны, выпущенные в 2019 году, поддерживают highp, но старые телефоны (2011, 2012, 2013) не поддерживают. - -Из спецификации: - -> The vertex language requires any uses of `lowp`, `mediump` and `highp` to compile and link without error. -> The fragment language requires any uses of `lowp` and `mediump` to compile without error. **Support for -> `highp` is optional**. - -Примеры мест, где обычно нужен highp. Точечные источники света с затенением по Фонгу обычно нуждаются в highp. Так что, например, вы можете использовать только направленные источники света на системе, которая не поддерживает highp, ИЛИ вы можете использовать только направленные источники света на мобильных для производительности. - -> Есть ли разница в производительности? - -Да, но как сказано выше, реализация может использовать более высокую точность. Так что если вы используете mediump на десктопном GPU, вы не увидите разницы в производительности, поскольку он действительно всегда использует highp. На мобильных вы увидите разницу в производительности, по крайней мере в 2019 году. Вы также можете увидеть, где вашим шейдерам действительно нужен highp. - -Вот шейдер Фонга, настроенный на использование mediump. На десктопе, поскольку mediump на самом деле highp, он работает: - -![](https://user-images.githubusercontent.com/234804/43352753-6ed6b9f8-9263-11e8-9716-3819a8c92095.png) - -На мобильных, где mediump действительно mediump, он ломается: - -![](https://user-images.githubusercontent.com/234804/43352759-7f9d1656-9263-11e8-8487-aa57d6092ff1.png) - -Пример, где mediump был бы хорош, по крайней мере во фрагментном шейдере — это большинство 2D игр. - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у - WayneNWayne - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md b/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md deleted file mode 100644 index 514a1c04c..000000000 --- a/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md +++ /dev/null @@ -1,121 +0,0 @@ -Title: Обход ограничений gl_PointSize в WebGL -Description: Как обойти ограничения gl_PointSize в WebGL -TOC: Обход ограничений gl_PointSize в WebGL - -## Вопрос: - -Я использую three.js для создания интерактивной визуализации данных. В этой визуализации рендерится 68000 узлов, каждый из которых имеет свой размер и цвет. - -Сначала я пытался делать это через рендеринг мешей, но это оказалось слишком дорого по производительности. Сейчас я использую систему частиц three.js, где каждая точка — это узел визуализации. - -Я могу управлять цветом и размером точки, но только до определённого предела. На моей видеокарте максимальный размер точки gl равен 63. Когда я увеличиваю масштаб визуализации, точки становятся больше — до определённого момента, а затем остаются 63 пикселя. - -Я использую собственные vertex и fragment шейдеры: - -vertex shader: - - attribute float size; - attribute vec3 ca; - varying vec3 vColor; - - void main() { - vColor = ca; - vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); - gl_PointSize = size * ( 300.0 / length( mvPosition.xyz ) ); - gl_Position = projectionMatrix * mvPosition; - } - -fragment shader: - - uniform vec3 color; - uniform sampler2D texture; - - varying vec3 vColor; - - void main() { - gl_FragColor = vec4( color * vColor, 1.0 ); - gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord ); - } - -Эти шейдеры почти дословно скопированы из одного из примеров three.js. - -Я совсем новичок в GLSL, но ищу способ рисовать точки больше 63 пикселей. Могу ли я, например, рисовать меш для точек больше определённого размера, а для остальных использовать gl_point? Есть ли другие способы обойти это ограничение и рисовать точки больше 63 пикселей? - -## Ответ: - -Вы можете сделать свою собственную систему точек, используя массивы unit quad'ов + центральную точку, а затем масштабировать их по размеру в GLSL. - -То есть, у вас будет 2 буфера. Один буфер — это просто 2D unitQuad, повторённый столько раз, сколько точек вы хотите нарисовать. - - var unitQuads = new Float32Array([ - -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, - -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, - -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, - -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, - -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, - ]); - -Второй буфер — это ваши точки, но позиции каждой точки нужно повторить 4 раза: - - var points = new Float32Array([ - p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, - p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, - p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, - p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, - p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, - ]); - -Настройте ваши буферы и атрибуты: - - var buf = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buf); - gl.bufferData(gl.ARRAY_BUFFER, unitQuads, gl.STATIC_DRAW); - gl.enableVertexAttribArray(unitQuadLoc); - gl.vertexAttribPointer(unitQuadLoc, 2, gl.FLOAT, false, 0, 0); - - var buf = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buf); - gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); - gl.enableVertexAttribArray(pointLoc); - gl.vertexAttribPointer(pointLoc, 3, gl.FLOAT, false, 0, 0); - -В вашем GLSL-шейдере вычисляйте нужный gl_PointSize, затем умножайте unitQuad на этот размер в view space или screen space. Screen space будет вести себя как обычный gl_Point, но часто хочется, чтобы точки масштабировались в 3D, как обычные объекты — тогда используйте view space. - - attribute vec2 a_unitQuad; - attribute vec4 a_position; - uniform mat4 u_view; - uniform mat4 u_viewProjection; - - void main() { - float fake_gl_pointsize = 150; - - // Получаем xAxis и yAxis во view space - // это unit-векторы, то есть направления, перпендикулярные взгляду - vec3 x_axis = view[0].xyz; - vec3 y_axis = view[1].xyz; - - // умножаем их на нужный размер - x_axis *= fake_gl_pointsize; - y_axis *= fake_gl_pointsize; - - // умножаем на unitQuad, чтобы получить quad вокруг центра - vec3 local_point = vec3(x_axis * a_unitQuad.x + y_axis * a_unitQuad.y); - - // добавляем позицию, где хотим разместить quad - local_point += a_position; - - // обычная математика для шейдера - gl_Position = u_viewProjection * local_point; - } - -Возможно, это звучит сложно, но есть рабочий пример [здесь][1] - - [1]: https://www.khronos.org/registry/webgl/sdk/demos/google/particles/ - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - Thomi - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md b/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md deleted file mode 100644 index 4cee2984d..000000000 --- a/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md +++ /dev/null @@ -1,60 +0,0 @@ -Title: Приближение и остановка у объекта в сцене WebGL -Description: Как реализовать приближение к объекту и остановку перед ним в сцене WebGL -TOC: Приближение и остановка у объекта в сцене WebGL - -## Вопрос: - -Мы создали WebGL-приложение, которое отображает сцену с несколькими объектами. Всю сцену можно вращать в разных направлениях. Требуется, чтобы пользователь мог приближаться к объекту, но НЕ проходить сквозь него. Я знаю, что такую функциональность можно реализовать с помощью фреймворков вроде Three.js и SceneJs. К сожалению, наше приложение не использует фреймворки. Можно ли реализовать описанное приближение только средствами WebGL? Примечание: object picking нам не подходит, так как пользователь не обязан выбирать объект в сцене. Спасибо за помощь. - -## Ответ: - -Навскидку: - -Во-первых, нужно знать размер каждого объекта в мировом пространстве. Например, если один объект размером 10 единиц, а другой — 100, вероятно, вы захотите находиться на разном расстоянии от них. Под мировым пространством я также имею в виду, что если вы масштабируете объект 10 на 9, то в мире он будет 90 единиц, и снова потребуется другое расстояние. - -Обычно размер объекта в локальном пространстве вычисляют по экстентам его вершин. Просто проходите по всем вершинам и отслеживайте минимальные и максимальные значения по x, y и z. Хотите ли вы брать максимальное значение от центра или вычислять реальный центр — решайте сами. - -Зная размер, можно вычислить, на каком расстоянии нужно быть, чтобы видеть весь объект. Для стандартной перспективной матрицы можно просто посчитать это в обратную сторону. Если объект 10 единиц, нужно уместить 10 единиц в фрустуме. Обычно берут чуть больше, например, 14 (size * 1.4), чтобы был запас вокруг объекта. - -![enter image description here][1] - -Известны `halfFovy`, `halfSizeToFitOnScreen`, нужно вычислить `distance`: - - sohcahtoa - tangent = opposite / adjacent - opposite = halfsizeToFitOnScreen - adjacent = distance - tangent = Math.tan(halfFovY) - -Следовательно: - - tangent = sizeToFitOnScreen / distance - tangent * distance = sizeToFitOnScreen - distance = sizeToFitOnScreen / tangent - distance = sizeToFitOnScreen / Math.tan(halfFovY) - -Теперь мы знаем, что камере нужно быть на расстоянии `distance` от объекта. На этом расстоянии вокруг объекта есть целая сфера. Где именно на этой сфере — решать вам. Обычно берут текущее положение камеры и вычисляют направление от объекта к камере: - - direction = normalize(cameraPos - objectPos) - -Теперь можно вычислить точку на расстоянии `distance` в этом направлении: - - desiredCameraPosition = direction * distance - -Теперь либо ставьте камеру туда с помощью lookAt: - - matrix = lookAt(desiredCameraPosition, objectPosition, up) - -Либо плавно перемещайте камеру (lerp) между текущей позицией и новой: - -{{{example url="../webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl-example-1.html"}}} - - [1]: http://i.stack.imgur.com/0axue.png - -
-
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у - jfc615 - с сайта - stackoverflow -
-
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna.md b/webgl/lessons/ru/webgl-qna.md deleted file mode 100644 index bc9e38917..000000000 --- a/webgl/lessons/ru/webgl-qna.md +++ /dev/null @@ -1,123 +0,0 @@ -Title: Вопросы и ответы -Description: Случайные вопросы и ответы -TOC: Вопросы и ответы - -Коллекция ссылок на случайные вопросы и/или ответы о темах, связанных с WebGL - -* [Эффект Дросте в WebGL](webgl-qna-webgl-droste-effect.html) -* [Обход ограничений gl_PointSize в WebGL](webgl-qna-working-around-gl_pointsize-limitations-webgl.html) -* [Как симулировать 3D текстуру в WebGL](webgl-qna-how-to-simulate-a-3d-texture-in-webgl.html) -* [Как рендерить изображения большого масштаба, такие как 32000x32000](webgl-qna-how-to-render-large-scale-images-like-32000x32000.html) -* [Эмуляция палитровой графики в WebGL](webgl-qna-emulating-palette-based-graphics-in-webgl.html) - ---- - -* [Простой способ показать нагрузку на вершинную и фрагментную обработку GPU?](webgl-qna-a-simple-way-to-show-the-load-on-the-gpu-s-vertex-and-fragment-processing-.html) -* [Как рисовать толстые линии в WebGL](https://mattdesl.svbtle.com/drawing-lines-is-hard) -* [FPS-подобное движение камеры с базовыми матричными трансформациями](webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.html) -* [Как использовать текстуру и цвет также в WebGL?](webgl-qna-how-to-use-texture--and-color-also-in-webgl-.html) -* [GLSL шейдер для поддержки раскрашивания и текстурирования](webgl-qna-glsl-shader-to-support-coloring-and-texturing.html) - ---- - -* [Как привязать массив текстур к uniform WebGL шейдера?](webgl-qna-how-to-bind-an-array-of-textures-to-a-webgl-shader-uniform-.html) -* [Передача значений альфа для каждого спрайта при батчинге](webgl-qna-passing-in-per-sprite-alpha-values-when-batching.html) -* [Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер?](webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.html) -* [WebGL 2D тайлмапы](webgl-qna-webgl-2d-tilemaps.html) -* [Определение минимальных/максимальных значений для всего изображения](webgl-qna-determine-min-max-values-for-the-entire-image.html) - ---- - -* [Рисование множества различных моделей в одном вызове draw](webgl-qna-drawing-many-different-models-in-a-single-draw-call.html) -* [Запись FPS в WebGL](webgl-qna-recording-fps-in-webgl.html) -* [Как написать веб-визуализатор музыки](webgl-qna-how-to-write-a-web-based-music-visualizer.html) -* [Как получить аудио данные в шейдер](webgl-qna-how-to-get-audio-data-into-a-shader.html) -* [Чистая пунктирная линия WebGL](webgl-qna-pure-webgl-dashed-line.html) -* [Масштабирование и остановка на объекте в сцене в WebGL](webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.html) - ---- - -* [Создание эффекта искажения изображения в WebGL](webgl-qna-create-image-warping-effect-in-webgl.html) -* [Создание эффекта размазывания/разжижения](webgl-qna-creating-a-smudge-liquify-effect.html) -* [Как получить эффект пикселизации в WebGL?](webgl-qna-how-to-get-pixelize-effect-in-webgl-.html) -* [Как сделать WebGL canvas прозрачным](webgl-qna-how-to-make-webgl-canvas-transparent.html) -* [Показать ночной вид против дневного вида на 3D сфере Земли](webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.html) - ---- - -* [Возможно ли измерить время рендеринга в WebGL используя gl.finish()?](webgl-qna-is-it-possible-to-measure-rendering-time-in-webgl-using-gl-finish---.html) -* [Эффективная система частиц в JavaScript? (WebGL)](webgl-qna-efficient-particle-system-in-javascript---webgl-.html) -* [Установка значений массива структур из JS в GLSL](webgl-qna-setting-the-values-of-a-struct-array-from-js-to-glsl.html) -* [Как затухать буфер рисования](webgl-qna-how-to-fade-the-drawing-buffer.html) -* [Рисование 2D изображения с картой глубины для достижения псевдо-3D эффекта](webgl-qna-drawing-2d-image-with-depth-map-to-achieve-pseudo-3d-effect.html) - ---- - -* [Как достичь движущейся линии с эффектами следа](webgl-qna-how-to-achieve-moving-line-with-trail-effects.html) -* [Как правильно рисовать текстурированные трапециевидные полигоны](webgl-qna-how-to-draw-correctly-textured-trapezoid-polygons.html) -* [Tex image TEXTURE_2D level 0 вызывает ленивую инициализацию](webgl-qna-tex-image-texture_2d-level-0-is-incurring-lazy-initialization.html) -* [Как реализовать масштабирование от мыши в 2D WebGL](webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.html) -* [Как создать тор](webgl-qna-how-to-create-a-torus.html) - ---- - -* [Не смешивать полигон, который пересекает сам себя](webgl-qna-don-t-blend-a-polygon-that-crosses-itself.html) -* [Как обнаружить обрезанные треугольники во фрагментном шейдере](webgl-qna-how-to-detect-clipped-triangles-in-the-framgment-shader.html) -* [Самый быстрый способ нарисовать много кругов](webgl-qna-the-fastest-way-to-draw-many-circles.html) -* [Сортировка и оптимизация инстансированного рендеринга](webgl-qna-sorting-and-optimizing-instanced-rendering.html) -* [Как определить, есть ли у изображения альфа-канал](webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.html) - ---- - -* [Как загружать изображения в фоне без рывков](webgl-qna-how-to-load-images-in-the-background-with-no-jank.html) -* [Когда выбирать highp, mediump, lowp в шейдерах](webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.html) -* [Как импортировать карту высот в WebGL](webgl-qna-how-to-import-a-heightmap-in-webgl.html) -* [Применение карты смещения и карты бликов](webgl-qna-apply-a-displacement-map-and-specular-map.html) -* [Как поддерживать и WebGL, и WebGL2](webgl-qna-how-to-support-both-webgl-and-webgl2.html) - ---- - -* [Как использовать буфер трафарета](webgl-qna-how-to-use-the-stencil-buffer.html) -* [Рисование карты высот](webgl-qna-drawing-a-heightmap.html) -* [Рисование текстурированных спрайтов с инстансированным рисованием](webgl-qna-drawing-textured-sprites-with-instanced-drawing.html) -* [Оптимизация рисования множества больших изображений](webgl-qna-optimize-drawing-lots-of-large-images.html) -* [Рендеринг медленно со временем](webgl-qna-rendering-slowly-over-time.html) - ---- - -* [Получить размер точки для проверки столкновений](webgl-qna-get-the-size-of-a-point-for-collision-checking.html) -* [Как получить 3D координаты клика мыши](webgl-qna-how-to-get-the-3d-coordinates-of-a-mouse-click.html) -* [Как смешивать цвета между 2 треугольниками](webgl-qna-how-to-blend-colors-across-2-triangles.html) -* [Как использовать текстуры как данные](webgl-qna-how-to-use-textures-as-data.html) -* [Как использовать прозрачность 2D спрайта как маску](webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.html) - ---- - -* [Как предотвратить просачивание текстур с атласом текстур](webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.html) -* [Как контролировать цвет между вершинами](webgl-qna-how-to-control-the-color-between-vertices.html) -* [Как сделать шейдер рыбьего глаза для скайбокса](webgl-qna-how-to-make-fisheye-skybox-shader.html) -* [Как получить автодополнение кода для WebGL в Visual Studio Code](webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.html) -* [Рисование слоев с разными точками](webgl-qna-drawing-layers-with-different-points.html) - ---- - -* [Могу ли я отключить предупреждение о том, что vertex attrib 0 отключен?](webgl-qna-can-i-mute-the-warning-about-vertex-attrib-0-being-disabled-.html) -* [Как сделать инструмент размазывающей кисти](webgl-qna-how-to-make-a-smudge-brush-tool.html) -* [Как читать один компонент с readPixels](webgl-qna-how-to-read-a-single-component-with-readpixels.html) -* [Я получаю ошибку недопустимого типа при вызове readPixels](webgl-qna-i-get-invalid-type-error-when-calling-readpixels.html) -* [Как оптимизировать рендеринг UI](webgl-qna-how-to-optimize-rendering-a-ui.html) - ---- - -* [Как я могу вычислить для 500 точек, какая из 1000 отрезков линий ближе всего к каждой точке?](webgl-qna-how-can-i-compute-for-500-points-which-of-1000-line-segments-is-nearest-to-each-point-.html) -* [Как я могу переместить точку схода перспективы из центра canvas?](webgl-qna-how-can-i-move-the-perspective-vanishing-point-from-the-center-of-the-canvas-.html) -* [Существует ли понятие обобщенного вершинного и фрагментного шейдера?](webgl-qna-is-there-the-notion-of-a-generalized-vertex-and-fragment-shader-.html) -* [Как определить среднюю яркость в сцене?](webgl-qna-how-to-determine-the-average-brightness-in-a-scene-.html) -* [Что такое локальное начало координат 3D модели?](webgl-qna-what-is-the-local-origin-of-a-3d-model-.html) - -## WebGL2 - -* [Доступ к текстурам по координатам пикселей в WebGL2](webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.html) -* [Как я могу получить все uniforms и uniformBlocks](webgl-qna-how-can-i-get-all-the-uniforms-and-uniformblocks.html) -* [Как я могу создать 16-битную гистограмму 16-битных данных](webgl-qna-how-can-i-create-a-16bit-historgram-of-16bit-data.html) -* [Как обрабатывать позиции частиц](webgl-qna-how-to-process-particle-positions.html) \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-render-to-texture.md b/webgl/lessons/ru/webgl-render-to-texture.md index 8bb564fcc..854521d52 100644 --- a/webgl/lessons/ru/webgl-render-to-texture.md +++ b/webgl/lessons/ru/webgl-render-to-texture.md @@ -252,80 +252,14 @@ gl.bindTexture(gl.TEXTURE_2D, depthTexture); В противном случае она не работает, и вам придется сделать что-то еще, например, сказать пользователю, что ему не повезло, или переключиться на какой-то другой метод. -Если вы еще не проверили [упрощение WebGL с меньшим количеством кода больше веселья](webgl-less-code-more-fun.html). +Если вы еще не проверили [упрощение WebGL с меньшим количеством кода, больше веселья](webgl-less-code-more-fun.html).

Сам Canvas на самом деле является текстурой

Это просто мелочь, но браузеры используют техники выше для реализации самого canvas. -За кулисами они создают цветную текстуру, буфер глубины, framebuffer, а затем они -привязывают его как текущий framebuffer. Вы делаете свой рендеринг, который рисует в эту текстуру. -Они затем используют эту текстуру для рендеринга вашего canvas в веб-страницу. -

-
- -``` -// создаем текстуру глубины -const depthTexture = gl.createTexture(); -gl.bindTexture(gl.TEXTURE_2D, depthTexture); - -// делаем буфер глубины того же размера, что и targetTexture -{ - // определяем размер и формат уровня 0 - const level = 0; - const internalFormat = gl.DEPTH_COMPONENT24; - const border = 0; - const format = gl.DEPTH_COMPONENT; - const type = gl.UNSIGNED_INT; - const data = null; - gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, - targetTextureWidth, targetTextureHeight, border, - format, type, data); - - // устанавливаем фильтрацию, чтобы нам не нужны были мипмапы - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - - // прикрепляем текстуру глубины к framebuffer - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, level); -} -``` - -И с этим вот результат. - -{{{example url="../webgl-render-to-texture-3-cubes-with-depth-buffer.html" }}} - -Теперь, когда у нас есть буфер глубины, прикрепленный к нашему framebuffer, внутренние кубы правильно пересекаются. - - - -Важно отметить, что WebGL гарантирует работу только определенных комбинаций вложений. -[Согласно спецификации](https://www.khronos.org/registry/webgl/specs/latest/1.0/#FBO_ATTACHMENTS) -единственные гарантированные комбинации вложений: - -* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура -* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура + `DEPTH_ATTACHMENT` = `DEPTH_COMPONENT16` renderbuffer -* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура + `DEPTH_STENCIL_ATTACHMENT` = `DEPTH_STENCIL` renderbuffer - -Для любых других комбинаций вы должны проверить, поддерживает ли система/GPU/драйвер/браузер пользователя эту комбинацию. -Для проверки вы создаете свой framebuffer, создаете и прикрепляете вложения, затем вызываете - - var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - -Если статус `FRAMEBUFFER_COMPLETE`, то эта комбинация вложений работает для этого пользователя. -В противном случае она не работает, и вам придется сделать что-то еще, например, сказать пользователю, что ему не повезло, -или переключиться на какой-то другой метод. - -Если вы еще не ознакомились с [упрощением WebGL с меньше кода больше веселья](webgl-less-code-more-fun.html). - -
-

Canvas сам по себе на самом деле текстура

-

-Это просто мелочь, но браузеры используют техники выше для реализации самого canvas. -За кулисами они создают цветную текстуру, буфер глубины, framebuffer, а затем они -привязывают его как текущий framebuffer. Вы делаете свой рендеринг, который рисует в эту текстуру. -Они затем используют эту текстуру для рендеринга вашего canvas в веб-страницу. +За кулисами они создают цветную текстуру, буфер глубины, framebuffer, а затем привязывают +его как текущий framebuffer. Вы делаете свой рендеринг, который рисует в эту текстуру. +Затем они используют эту текстуру для рендеринга вашего canvas в веб-страницу.

\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-resizing-the-canvas.md b/webgl/lessons/ru/webgl-resizing-the-canvas.md index f5828a93f..784cee81a 100644 --- a/webgl/lessons/ru/webgl-resizing-the-canvas.md +++ b/webgl/lessons/ru/webgl-resizing-the-canvas.md @@ -201,4 +201,277 @@ function resizeCanvasToDisplaySize(canvas) { return needResize; } -``` \ No newline at end of file +``` + +Нам нужно вызвать `Math.round` (или `Math.ceil`, или `Math.floor` или `| 0`), чтобы получить число +к целому, потому что `canvas.width` и `canvas.height` всегда в целых числах, поэтому +наше сравнение может не сработать, если `devicePixelRatio` не является целым числом, что часто встречается, особенно +если пользователь масштабирует. + +> Примечание: Использовать ли `Math.floor` или `Math.ceil` или `Math.round` не определено HTML +спецификацией. Это зависит от браузера. 🙄 + +В любом случае, это **не** будет работать на самом деле. Новая проблема в том, что при `devicePixelRatio`, который не равен 1.0, +CSS размер, который нужен canvas, чтобы заполнить данную область, может не быть целым значением, +но `clientWidth` и `clientHeight` определены как целые числа. Допустим, окно +999 фактических пикселей устройства в ширину, ваш devicePixelRatio = 2.0, и вы просите canvas размером 100%. +Нет целого CSS размера * 2.0, который = 999. + +Следующее решение - использовать +[`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). +Он возвращает [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect), +который имеет `width` и `height`. Это тот же +client rect, который представлен `clientWidth` и `clientHeight`, но он не обязан +быть целым числом. + +Ниже фиолетовый ``, который установлен на `width: 100%` его контейнера. Уменьшите масштаб несколько раз до 75% или 60% +и вы можете увидеть, как его `clientWidth` и его `getBoundingClientRect().width` расходятся. + +>
+ +На моих машинах я получаю эти показания + +``` +Windows 10, уровень масштабирования 75%, Chrome +clientWidth: 700 +getBoundingClientRect().width = 700.0000610351562 + +MacOS, уровень масштабирования 90%, Chrome +clientWidth: 700 +getBoundingClientRect().width = 700.0000610351562 + +MacOS, уровень масштабирования -1, Safari (safari не показывает уровень масштабирования) +clientWidth: 700 +getBoundingClientRect().width = 699.9999389648438 + +Firefox, и Windows, и MacOS все уровни масштабирования +clientWidth: 700 +getBoundingClientRect().width = 700 +``` + +Примечание: Firefox показал 700 в этой конкретной настройке, но при достаточном количестве различных тестов я +видел, что он дает нецелый результат из `getBoundingClientRect`, например, сделайте окно +тонким, чтобы 100% canvas был меньше 700, и вы можете получить нецелый результат +в Firefox. + +Итак, учитывая это, мы могли бы попробовать использовать `getBoundingClientRect`. + +```js +function resizeCanvasToDisplaySize(canvas) { + // Ищем размер, в котором браузер отображает canvas в CSS пикселях. + const dpr = window.devicePixelRatio; + const {width, height} = canvas.getBoundingClientRect(); + const displayWidth = Math.round(width * dpr); + const displayHeight = Math.round(height * dpr); + + // Проверяем, не является ли canvas того же размера. + const needResize = canvas.width != displayWidth || + canvas.height != displayHeight; + + if (needResize) { + // Делаем canvas того же размера + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + return needResize; +} +``` + +Итак, мы закончили? К сожалению, нет. Оказывается, что `canvas.getBoundingClientRect()` может +не всегда возвращать точно правильный размер. Причина сложная, но она связана с +тем, как браузер решает рисовать вещи. Некоторые части решаются на уровне HTML, +а некоторые части решаются позже на уровне "композитора" (часть, которая фактически рисует). +`getBoundingClientRect()` происходит на уровне HTML, но определенные вещи происходят после этого, +что может повлиять на то, какого размера canvas фактически рисуется. + +Я думаю, пример в том, что HTML часть работает в абстрактном, а композитор +работает в конкретном. Итак, допустим, у вас есть окно шириной 999 пикселей устройства +и devicePixelRatio 2.0. Вы делаете два элемента рядом, которые +`width: 50%`. Итак, HTML вычисляет, что каждый должен быть 499.5 пикселей устройства. Но когда +фактически приходит время рисовать, композитор не может нарисовать 499.5 пикселей, поэтому один +элемент получает 499, а другой получает 500. Какой получает или теряет пиксель, +не определено никакими спецификациями. + +Решение, которое придумали производители браузеров, - использовать +[`ResizeObserver` API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) +и предоставить фактический используемый размер через свойство `devicePixelContextBoxSize` +записей, которые он предоставляет. +Он возвращает фактическое количество пикселей устройства, которые были использованы. Обратите внимание, что это называется +`ContentBox`, а не `ClientBox`, что означает, что это фактическая часть +canvas элемента, показывающая *содержимое* canvas, поэтому она не включает padding, как +`clientWidth`, `clientHeight` и `getBoundingClientRect`, приятное преимущество. + +Это возвращается таким образом, потому что результат асинхронный. "Композитор", упомянутый +выше, работает асинхронно от страницы. Он может выяснить размер, который он фактически собирается использовать, а затем отправить вам этот размер *вне полосы*. + +К сожалению, хотя `ResizeObserver` доступен во всех современных браузерах, +`devicePixelContentBoxSize` пока доступен только в Chrome/Edge. Вот как +его использовать. + +Мы создаем `ResizeObserver` и передаем ему функцию для вызова в любое время, когда любой элемент, +который мы наблюдаем, изменяет размер. В нашем случае это наш canvas. + +```js +const resizeObserver = new ResizeObserver(onResize); +resizeObserver.observe(canvas, {box: 'content-box'}); +``` + +Код выше создает `ResizeObserver`, который будет вызывать функцию `onResize` +(ниже), когда элемент, который мы наблюдаем, изменяет размер. Мы говорим ему `observe` наш +canvas. Мы говорим ему наблюдать, когда `content-box` изменяет размер. Это +важно и немного запутанно. Мы могли бы попросить его сказать нам, когда +`device-pixel-content-box` изменяет размер, но давайте представим, что у нас есть canvas, который +имеет какой-то процентный размер окна, как обычные 100% нашего примера с линией +выше. В этом случае наш canvas всегда будет иметь то же количество пикселей устройства +независимо от уровня масштабирования. Окно не изменило размер, когда мы масштабируем, поэтому все еще +то же количество пикселей устройства. С другой стороны, `content-box` будет +изменяться, когда мы масштабируем, потому что он измеряется в CSS пикселях, и поэтому, когда мы масштабируем, больше или +меньше CSS пикселей помещается в количество пикселей устройства. + +Если нас не волнует уровень масштабирования, то мы могли бы просто наблюдать `device-pixel-content-box`. +Это выбросит ошибку, если это не поддерживается, поэтому мы сделали бы что-то вроде этого + +```js +const resizeObserver = new ResizeObserver(onResize); +try { + // вызывать нас только если количество пикселей устройства изменилось + resizeObserver.observe(canvas, {box: 'device-pixel-content-box'}); +} catch (ex) { + // device-pixel-content-box не поддерживается, поэтому откатываемся к этому + resizeObserver.observe(canvas, {box: 'content-box'}); +} +``` + +Функция `onResize` будет вызвана с массивом [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry). Один для каждой вещи, +которая изменила размер. Мы запишем размер в карту, чтобы мы могли обработать больше чем +один элемент. + +```js +// инициализируем с размером canvas по умолчанию +const canvasToDisplaySizeMap = new Map([[canvas, [300, 150]]]); + +function onResize(entries) { + for (const entry of entries) { + let width; + let height; + let dpr = window.devicePixelRatio; + if (entry.devicePixelContentBoxSize) { + // ПРИМЕЧАНИЕ: Только этот путь дает правильный ответ + // Другие пути - несовершенные откаты + // для браузеров, которые не предоставляют никакого способа сделать это + width = entry.devicePixelContentBoxSize[0].inlineSize; + height = entry.devicePixelContentBoxSize[0].blockSize; + dpr = 1; // это уже в width и height + } else if (entry.contentBoxSize) { + if (entry.contentBoxSize[0]) { + width = entry.contentBoxSize[0].inlineSize; + height = entry.contentBoxSize[0].blockSize; + } else { + width = entry.contentBoxSize.inlineSize; + height = entry.contentBoxSize.blockSize; + } + } else { + width = entry.contentRect.width; + height = entry.contentRect.height; + } + const displayWidth = Math.round(width * dpr); + const displayHeight = Math.round(height * dpr); + canvasToDisplaySizeMap.set(entry.target, [displayWidth, displayHeight]); + } +} +``` + +Это своего рода беспорядок. Вы можете видеть, что API отправил по крайней мере 3 разные версии +перед поддержкой `devicePixelContentBoxSize` 😂 + +Теперь мы изменим нашу функцию resize, чтобы использовать эти данные + +```js +function resizeCanvasToDisplaySize(canvas) { + // Получаем размер, в котором браузер отображает canvas в пикселях устройства. + const [displayWidth, displayHeight] = canvasToDisplaySizeMap.get(canvas); + + // Проверяем, не является ли canvas того же размера. + const needResize = canvas.width != displayWidth || + canvas.height != displayHeight; + + if (needResize) { + // Делаем canvas того же размера + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + return needResize; +} +``` + +Вот пример использования этого кода + +{{{example url="../webgl-resize-canvas-hd-dpi.html" }}} + +Может быть трудно увидеть какую-либо разницу. Если у вас есть HD-DPI дисплей, +как ваш смартфон или все Mac с 2019 года, или, возможно, 4k монитор, то эта +линия должна быть тоньше, чем линия предыдущего примера. + +В противном случае, если вы увеличиваете масштаб (я предлагаю открыть пример в новом окне), когда вы увеличиваете масштаб, +линия должна оставаться того же разрешения, тогда как если вы увеличиваете масштаб в предыдущем примере, +линия станет толще и с более низким разрешением, поскольку она не настраивается на `devicePixelRatio`. + +Просто как тест, вот все 3 метода выше, используя простой canvas 2d. +Чтобы было просто, он не использует WebGL. Вместо этого он использует Canvas 2D и делает 2 паттерна, 2x2 пиксельный вертикальный черно-белый паттерн и 2x2 пиксельный горизонтальный черно-белый +паттерн. Он рисует горизонтальный паттерн ▤ слева и вертикальный паттерн ▥ +справа. + +{{{example url="../webgl-resize-the-canvas-comparison.html"}}} + +Измените размер этого окна, или лучше, откройте его в новом окне и увеличивайте и уменьшайте масштаб, используя +клавиши, упомянутые выше. На разных уровнях масштабирования измените размер окна и обратите внимание, +что только нижний работает во всех случаях (в Chrome/Edge). Обратите внимание, что чем выше +`devicePixelRatio` вашего устройства, тем труднее может быть увидеть проблемы. Что вы +должны увидеть - это неизменяющийся паттерн слева и справа. Если вы видите резкие +паттерны или вы видите различающуюся темноту, как градиент, то это не работает. +Поскольку это будет работать только в Chrome/Edge, вам нужно попробовать это там, чтобы увидеть, как это работает. + +Также обратите внимание, что некоторые ОС (MacOS) предоставляют опцию масштабирования на уровне ОС, которая в основном +скрыта от приложений. В этом случае вы увидите легкий паттерн в нижнем примере +(предполагая, что вы в Chrome/Edge), но это будет регулярный паттерн. + +Это поднимает проблему, что нет хорошего решения в других браузерах, но нужен ли вам +настоящее решение? Большинство WebGL приложений делают что-то вроде рисования некоторых вещей в 3D +с текстурами и/или освещением на них. Как таковые, часто не заметно, использовать ли +верхнее решение, где мы игнорировали `devicePixelRatio`, или использовать `clientWidth`, `clientHeight` +или `getBoundingClientRect()` * `devicePixelRatio` и не беспокоиться об этом дальше. + +Кроме того, слепое использование `devicePixelRatio` может действительно замедлить вашу производительность. +На iPhoneX или iPhone11 window.devicePixelRatio равен 3, что +означает, что вы будете рисовать в 9 раз больше пикселей. На Samsung Galaxy S8 это значение 4, что означает, что вы будете рисовать +в 16 раз больше пикселей. Это может действительно замедлить вашу программу. Фактически, это обычная оптимизация в играх - фактически рендерить +меньше пикселей, чем отображается, и позволить GPU масштабировать их вверх. Это действительно зависит от ваших потребностей. Если вы рисуете +график для печати, вы можете захотеть поддержать HD-DPI. Если вы делаете игру, вы можете не хотеть или можете захотеть дать +пользователю возможность включить или выключить поддержку, если их система не достаточно быстра, чтобы рисовать так много пикселей. + +Еще одна оговорка заключается в том, что по крайней мере в январе 2021 года `round(getBoundingClientRect * devicePixelRatio)` работает во всех современных браузерах **ЕСЛИ И ТОЛЬКО ЕСЛИ** canvas имеет полный +размер окна, как в примере с линией выше. Вот пример использования паттернов + +{{{example url="../webgl-resize-the-canvas-comparison-fullwindow.html"}}} + +Вы заметите, что если вы масштабируете и изменяете размер *этой страницы*, это не сработает с `getBoundingClientRect`. +Это потому, что canvas не является полным окном, он находится в iframe. Откройте пример +в отдельном окне, и это будет работать. + +Какое решение вы используете, зависит от вас. Для меня 99% времени я не использую +`devicePixelRatio`. Это делает мои страницы медленными, и кроме нескольких графических профессионалов большинство +людей не заметят разницы. На этом сайте есть несколько диаграмм, где это используется, но большинство примеров не используют. + +Если вы посмотрите на многие WebGL программы, они обрабатывают изменение размера или установку размера canvas многими различными способами. +Я думаю, что спорно, что лучший способ - позволить браузеру выбрать размер для отображения canvas с помощью CSS, а затем посмотреть, какой размер он выбрал, и настроить +количество пикселей в canvas в ответ. +Если вам любопытно, вот некоторые из причин, по которым я думаю, что описанный выше способ является предпочтительным. + + + + + + \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-scene-graph.md b/webgl/lessons/ru/webgl-scene-graph.md index 87a68a86e..87635b21c 100644 --- a/webgl/lessons/ru/webgl-scene-graph.md +++ b/webgl/lessons/ru/webgl-scene-graph.md @@ -251,18 +251,18 @@ TOC: Графы сцен | луна -Это позволит Земле вращаться вокруг солнечнойСистемы, но мы можем отдельно вращать и масштабировать Солнце, и это не будет +Это позволит Земле вращаться вокруг солнечной системы, но мы можем отдельно вращать и масштабировать Солнце, и это не будет влиять на Землю. Аналогично Земля может вращаться отдельно от Луны. Давайте создадим больше узлов для `solarSystem`, `earthOrbit` и `moonOrbit`. var solarSystemNode = new Node(); var earthOrbitNode = new Node(); - // орбита земли в 100 единицах от солнца + // орбита Земли в 100 единицах от Солнца earthOrbitNode.localMatrix = m4.translation(100, 0, 0); var moonOrbitNode = new Node(); - // луна в 20 единицах от земли + // луна в 20 единицах от Земли moonOrbitNode.localMatrix = m4.translation(20, 0, 0); Те расстояния орбит были удалены из старых узлов @@ -287,7 +287,7 @@ TOC: Графы сцен moonOrbitNode.setParent(earthOrbitNode); moonNode.setParent(moonOrbitNode); -И нам нужно обновлять только орбиты +И нам нужно только обновить орбиты // обновляем локальные матрицы для каждого объекта. -m4.multiply(m4.yRotation(0.01), sunNode.localMatrix , sunNode.localMatrix); @@ -332,9 +332,9 @@ TOC: Графы сцен {{{example url="../webgl-scene-graph-solar-system-adjusted.html" }}} -В настоящее время у нас есть `localMatrix`, и мы модифицируем его каждый кадр. Однако есть проблема +В настоящее время у нас есть `localMatrix`, и мы изменяем его каждый кадр. Однако есть проблема в том, что каждый кадр наша математика будет накапливать небольшую ошибку. Есть способ исправить математику, -который называется *ортогональной нормализацией матрицы*, но даже это не всегда будет работать. Например, давайте +который называется *ортогональная нормализация матрицы*, но даже это не всегда работает. Например, давайте представим, что мы масштабировали до нуля и обратно. Давайте просто сделаем это для одного значения `x` x = 246; // кадр #0, x = 246 @@ -355,8 +355,8 @@ TOC: Графы сцен x = x * scale // кадр #5, x = 0 УПС! Мы потеряли наше значение. Мы можем исправить это, добавив какой-то другой класс, который обновляет матрицу из -других значений. Давайте изменим определение `Node`, чтобы иметь `source`. Если он существует, мы будем -просить `source` дать нам локальную матрицу. +других значений. Давайте изменим определение `Node`, чтобы иметь `source`. Если он существует, мы +попросим `source` дать нам локальную матрицу. *var Node = function(source) { this.children = []; @@ -374,7 +374,7 @@ TOC: Графы сцен ... -Теперь мы можем создать источник. Общий источник - это тот, который предоставляет перемещение, поворот и масштаб +Теперь мы можем создать источник. Общий источник - это тот, который предоставляет перемещение, поворот и масштабирование, что-то вроде этого var TRS = function() { @@ -389,7 +389,7 @@ TOC: Графы сцен var r = this.rotation; var s = this.scale; - // вычисляем матрицу из перемещения, поворота и масштаба + // вычисляем матрицу из перемещения, поворота и масштабирования m4.translation(t[0], t[1], t[2], dst); m4.xRotate(dst, r[0], dst); m4.yRotate(dst, r[1], dst); @@ -407,7 +407,7 @@ TOC: Графы сцен // во время рендеринга someTRS.rotation[2] += elapsedTime; -Теперь нет проблемы, потому что мы воссоздаем матрицу каждый раз. +Теперь нет проблем, потому что мы воссоздаем матрицу каждый раз. Вы можете думать, я не создаю солнечную систему, так в чем смысл? Ну, если вы хотели бы анимировать человека, вы могли бы иметь граф сцены, который выглядит так @@ -418,7 +418,7 @@ TOC: Графы сцен тем больше мощности требуется для вычисления анимаций и тем больше данных анимации требуется для предоставления информации для всех суставов. Старые игры, такие как Virtua Fighter, имели около 15 суставов. Игры в начале-середине 2000-х имели от 30 до 70 суставов. Если бы вы сделали каждый сустав в ваших руках, -там по крайней мере 20 в каждой руке, так что только 2 руки - это 40 суставов. Многие игры, которые хотят +их по крайней мере 20 в каждой руке, поэтому только 2 руки - это 40 суставов. Многие игры, которые хотят анимировать руки, анимируют большой палец как один и 4 пальца как один большой палец, чтобы сэкономить время (как CPU/GPU, так и время художника) и память. @@ -427,7 +427,7 @@ TOC: Графы сцен {{{example url="../webgl-scene-graph-block-guy.html" }}} -Если вы посмотрите практически на любую 3D библиотеку, вы найдете граф сцены, подобный этому. +Если вы посмотрите практически на любую 3D библиотеку, вы найдете граф сцены, похожий на этот. Что касается построения иерархий, обычно они создаются в каком-то пакете моделирования или пакете компоновки уровней. @@ -435,8 +435,8 @@ TOC: Графы сцен

SetParent vs AddChild / RemoveChild

Многие графы сцен имеют функцию node.addChild и функцию node.removeChild, тогда как выше я создал функцию node.setParent. Какой способ лучше -спорно является вопросом стиля, но я бы утверждал, что есть одна объективно лучшая причина -setParent лучше, чем addChild, заключается в том, что это делает код, подобный +спорно является вопросом стиля, но я бы утверждал, что есть одна объективно лучшая причина, +почему setParent лучше, чем addChild, заключается в том, что это делает код, подобный этому, невозможным.

{{#escapehtml}}
     someParent.addChild(someNode);
@@ -445,8 +445,8 @@ TOC: Графы сцен
 {{/escapehtml}}

Что это означает? Добавляется ли someNode к обоим someParent и someOtherParent? В большинстве графов сцен это невозможно. Генерирует ли второй вызов ошибку? -ERROR: Already have parent. Магически ли он удаляет someNode из someParent перед -добавлением к someOtherParent? Если да, то это, конечно, не ясно из имени addChild. +ERROR: Already have parent. Удаляет ли он магически someNode из someParent перед +добавлением к someOtherParent? Если да, то это, конечно, не ясно из названия addChild.

setParent с другой стороны не имеет такой проблемы

{{#escapehtml}}
diff --git a/webgl/lessons/ru/webgl-shaders-and-glsl.md b/webgl/lessons/ru/webgl-shaders-and-glsl.md
index de3b66de8..b1ed83b63 100644
--- a/webgl/lessons/ru/webgl-shaders-and-glsl.md
+++ b/webgl/lessons/ru/webgl-shaders-and-glsl.md
@@ -196,4 +196,233 @@ Uniforms могут быть многих типов. Для каждого ти
 
     void main() {
        outColor = doMathToMakeAColor;
-    } 
\ No newline at end of file
+    } 
+
+Ваш фрагментный шейдер вызывается один раз для каждого пикселя. Каждый раз, когда он вызывается, вы обязаны
+установить вашу out переменную в какой-то цвет.
+
+Фрагментным шейдерам нужны данные. Они могут получить данные 3 способами
+
+1.  [Uniforms](#uniforms) (значения, которые остаются одинаковыми для каждого пикселя одного вызова рисования)
+2.  [Текстуры](#textures-in-fragment-shaders) (данные из пикселей/текселей)
+3.  [Varyings](#varyings) (данные, передаваемые из вершинного шейдера и интерполированные)
+
+### Uniforms в фрагментных шейдерах
+
+См. [Uniforms в вершинных шейдерах](#uniforms).
+
+### Текстуры в фрагментных шейдерах
+
+Чтобы получить значение из текстуры в шейдере, мы создаем uniform `sampler2D` и используем GLSL
+функцию `texture` для извлечения значения из неё.
+
+    precision highp float;
+
+    uniform sampler2D u_texture;
+
+    out vec4 outColor;
+
+    void main() {
+       vec2 texcoord = vec2(0.5, 0.5);  // получить значение из середины текстуры
+       outColor = texture(u_texture, texcoord);
+    }
+
+Какие данные выходят из текстуры, [зависит от многих настроек](webgl-3d-textures.html).
+Как минимум нам нужно создать и поместить данные в текстуру, например
+
+    var tex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, tex);
+    var level = 0;
+    var internalFormat = gl.RGBA,
+    var width = 2;
+    var height = 1;
+    var border = 0; // ВСЕГДА ДОЛЖЕН БЫТЬ НУЛЕМ
+    var format = gl.RGBA;
+    var type = gl.UNSIGNED_BYTE;
+    var data = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
+    gl.texImage2D(gl.TEXTURE_2D,
+                  level,
+                  internalFormat,
+                  width,
+                  height,
+                  border,
+                  format,
+                  type,
+                  data);
+
+Установить фильтрацию
+
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+
+Затем найти местоположение uniform в шейдерной программе
+
+    var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");
+
+WebGL затем требует, чтобы вы привязали его к текстуре unit
+
+    var unit = 5;  // Выберите какой-то текстуре unit
+    gl.activeTexture(gl.TEXTURE0 + unit);
+    gl.bindTexture(gl.TEXTURE_2D, tex);
+
+И сказать шейдеру, к какому unit вы привязали текстуру
+
+    gl.uniform1i(someSamplerLoc, unit);
+
+### Varyings
+
+Varying - это способ передать значение из вершинного шейдера в фрагментный шейдер, что мы
+покрыли в [как это работает](webgl-how-it-works.html).
+
+Чтобы использовать varying, нам нужно объявить соответствующие varyings в вершинном и фрагментном шейдере.
+Мы устанавливаем *out* varying в вершинном шейдере с некоторым значением для каждой вершины. Когда WebGL рисует пиксели,
+он будет опционально интерполировать между этими значениями и передавать их соответствующему *in* varying в
+фрагментном шейдере
+
+Вершинный шейдер
+
+    #version 300 es
+
+    in vec4 a_position;
+
+    uniform vec4 u_offset;
+
+    +out vec4 v_positionWithOffset;
+
+    void main() {
+      gl_Position = a_position + u_offset;
+    +  v_positionWithOffset = a_position + u_offset;
+    }
+
+Фрагментный шейдер
+
+    #version 300 es
+    precision highp float;
+
+    +in vec4 v_positionWithOffset;
+
+    out vec4 outColor;
+
+    void main() {
+    +  // конвертируем из clip space (-1 <-> +1) в цветовое пространство (0 -> 1).
+    +  vec4 color = v_positionWithOffset * 0.5 + 0.5;
+    +  outColor = color;
+    }
+
+Пример выше - это в основном бессмысленный пример. Обычно не имеет смысла
+напрямую копировать значения clip space в фрагментный шейдер и использовать их как цвета. Тем не менее
+это будет работать и производить цвета.
+
+## GLSL
+
+GLSL означает [Graphics Library Shader Language](https://www.khronos.org/registry/gles/specs/3.0/GLSL_ES_Specification_3.00.3.pdf).
+Это язык, на котором написаны шейдеры. У него есть некоторые специальные полууникальные особенности, которые, конечно, не распространены в JavaScript.
+Он разработан для выполнения математики, которая обычно нужна для вычисления вещей для растеризации
+графики. Так, например, у него есть встроенные типы, такие как `vec2`, `vec3` и `vec4`, которые
+представляют 2 значения, 3 значения и 4 значения соответственно. Аналогично у него есть `mat2`, `mat3`
+и `mat4`, которые представляют матрицы 2x2, 3x3 и 4x4. Вы можете делать такие вещи, как умножать
+`vec` на скаляр.
+
+    vec4 a = vec4(1, 2, 3, 4);
+    vec4 b = a * 2.0;
+    // b теперь vec4(2, 4, 6, 8);
+
+Аналогично он может делать умножение матриц и умножение вектора на матрицу
+
+    mat4 a = ???
+    mat4 b = ???
+    mat4 c = a * b;
+
+    vec4 v = ???
+    vec4 y = c * v;
+
+У него также есть различные селекторы для частей vec. Для vec4
+
+    vec4 v;
+
+*   `v.x` то же самое, что `v.s` и `v.r` и `v[0]`.
+*   `v.y` то же самое, что `v.t` и `v.g` и `v[1]`.
+*   `v.z` то же самое, что `v.p` и `v.b` и `v[2]`.
+*   `v.w` то же самое, что `v.q` и `v.a` и `v[3]`.
+
+Он способен *swizzle* компоненты vec, что означает, что вы можете поменять или повторить компоненты.
+
+    v.yyyy
+
+то же самое, что
+
+    vec4(v.y, v.y, v.y, v.y)
+
+Аналогично
+
+    v.bgra
+
+то же самое, что
+
+    vec4(v.b, v.g, v.r, v.a)
+
+При конструировании vec или mat вы можете предоставить несколько частей сразу. Так, например
+
+    vec4(v.rgb, 1)
+
+То же самое, что
+
+    vec4(v.r, v.g, v.b, 1)
+
+Одна вещь, на которой вы, вероятно, застрянете, это то, что GLSL очень строго типизирован.
+
+    float f = 1;  // ОШИБКА 1 это int. Вы не можете присвоить int к float
+
+Правильный способ - один из этих
+
+    float f = 1.0;      // использовать float
+    float f = float(1)  // привести целое число к float
+
+Пример выше `vec4(v.rgb, 1)` не жалуется на `1`, потому что `vec4` приводит
+вещи внутри, как `float(1)`.
+
+GLSL имеет кучу встроенных функций. Многие из них работают с несколькими компонентами сразу.
+Так, например
+
+    T sin(T angle)
+
+Означает, что T может быть `float`, `vec2`, `vec3` или `vec4`. Если вы передаете `vec4`, вы получаете `vec4` обратно,
+который является синусом каждого из компонентов. Другими словами, если `v` это `vec4`, то
+
+    vec4 s = sin(v);
+
+то же самое, что
+
+    vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));
+
+Иногда один аргумент - это float, а остальные - `T`. Это означает, что этот float будет применен
+ко всем компонентам. Например, если `v1` и `v2` это `vec4`, а `f` это float, то
+
+    vec4 m = mix(v1, v2, f);
+
+то же самое, что
+
+    vec4 m = vec4(
+      mix(v1.x, v2.x, f),
+      mix(v1.y, v2.y, f),
+      mix(v1.z, v2.z, f),
+      mix(v1.w, v2.w, f));
+
+Вы можете увидеть список всех GLSL функций на последних 3 страницах [OpenGL ES 3.0
+Reference Card](https://www.khronos.org/files/opengles3-quick-reference-card.pdf)
+Если вам нравится действительно сухой и многословный материал, вы можете попробовать
+[GLSL ES 3.00 spec](https://www.khronos.org/registry/gles/specs/3.0/GLSL_ES_Specification_3.00.3.pdf).
+
+## Собираем все вместе
+
+В этом суть всей этой серии постов. WebGL - это все о создании различных шейдеров, предоставлении
+данных этим шейдерам и затем вызове `gl.drawArrays`, `gl.drawElements` и т.д., чтобы WebGL обработал
+вершины, вызывая текущий вершинный шейдер для каждой вершины, а затем рендерил пиксели, вызывая текущий фрагментный шейдер для каждого пикселя.
+
+Фактически создание шейдеров требует нескольких строк кода. Поскольку эти строки одинаковы в
+большинстве WebGL программ и поскольку однажды написанные, вы можете в значительной степени игнорировать их, [как компилировать GLSL шейдеры
+и связывать их в шейдерную программу, покрыто здесь](webgl-boilerplate.html).
+
+Если вы только начинаете отсюда, вы можете пойти в 2 направлениях. Если вас интересует обработка изображений,
+я покажу вам [как делать некоторую 2D обработку изображений](webgl-image-processing.html).
+Если вас интересует изучение перемещения,
+поворота и масштабирования, то [начните здесь](webgl-2d-translation.html). 
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-shadows.md b/webgl/lessons/ru/webgl-shadows.md
index 9a3ca57bb..27558d62e 100644
--- a/webgl/lessons/ru/webgl-shadows.md
+++ b/webgl/lessons/ru/webgl-shadows.md
@@ -196,4 +196,712 @@ gl.framebufferTexture2D(
   gl.bindAttribLocation(program, 1, "a_texcoord");
   gl.linkProgram(program);
   ...
-  ``` 
\ No newline at end of file
+  ```
+
+Мы будем использовать этот второй способ, поскольку он более [D.R.Y](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
+
+Библиотека, которую мы используем для компиляции и связывания наших шейдеров, имеет опцию сделать это
+для нас. Мы просто передаем ей имена атрибутов и их местоположения, и она
+вызовет `gl.bindAttribLocation` для нас
+
+```js
+// настраиваем GLSL программы
++// примечание: Поскольку мы собираемся использовать один и тот же VAO с несколькими
++// программами шейдеров, нам нужно убедиться, что все программы используют
++// одинаковые местоположения атрибутов. Есть 2 способа сделать это.
++// (1) назначить их в GLSL. (2) назначить их, вызвав `gl.bindAttribLocation`
++// перед связыванием. Мы используем метод 2, поскольку он более D.R.Y.
++const programOptions = {
++  attribLocations: {
++    'a_position': 0,
++    'a_normal':   1,
++    'a_texcoord': 2,
++    'a_color':    3,
++  },
++};
+-const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs]);
+-const colorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS],);
++const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs], programOptions);
++const colorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS], programOptions);
+```
+
+Теперь давайте используем `drawScene`, чтобы нарисовать сцену с точки зрения источника света,
+а затем снова с текстурой глубины
+
+```js
+function render() {
+  twgl.resizeCanvasToDisplaySize(gl.canvas);
+
+  gl.enable(gl.CULL_FACE);
+  gl.enable(gl.DEPTH_TEST);
+
+  // сначала рисуем с точки зрения источника света
+-  const textureWorldMatrix = m4.lookAt(
++  const lightWorldMatrix = m4.lookAt(
+      [settings.posX, settings.posY, settings.posZ],          // позиция
+      [settings.targetX, settings.targetY, settings.targetZ], // цель
+      [0, 1, 0],                                              // вверх
+  );
+-  const textureProjectionMatrix = settings.perspective
++  const lightProjectionMatrix = settings.perspective
+      ? m4.perspective(
+          degToRad(settings.fieldOfView),
+          settings.projWidth / settings.projHeight,
+          0.5,  // near
+          10)   // far
+      : m4.orthographic(
+          -settings.projWidth / 2,   // left
+           settings.projWidth / 2,   // right
+          -settings.projHeight / 2,  // bottom
+           settings.projHeight / 2,  // top
+           0.5,                      // near
+           10);                      // far
+
++  // рисуем в текстуру глубины
++  gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer);
++  gl.viewport(0, 0, depthTextureSize, depthTextureSize);
++  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+-  drawScene(textureProjectionMatrix, textureWorldMatrix, m4.identity());
++  drawScene(lightProjectionMatrix, lightWorldMatrix, m4.identity(), colorProgramInfo);
+
++  // теперь рисуем сцену на canvas, проецируя текстуру глубины в сцену
++  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
++  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
++  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+  let textureMatrix = m4.identity();
+  textureMatrix = m4.translate(textureMatrix, 0.5, 0.5, 0.5);
+  textureMatrix = m4.scale(textureMatrix, 0.5, 0.5, 0.5);
+-  textureMatrix = m4.multiply(textureMatrix, textureProjectionMatrix);
++  textureMatrix = m4.multiply(textureMatrix, lightProjectionMatrix);
+  // используем обратную этой мировой матрицы, чтобы сделать
+  // матрицу, которая будет преобразовывать другие позиции
+  // чтобы быть относительными к этому мировому пространству.
+  textureMatrix = m4.multiply(
+      textureMatrix,
+-      m4.inverse(textureWorldMatrix));
++      m4.inverse(lightWorldMatrix));
+
+  // Вычисляем матрицу проекции
+  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
+  const projectionMatrix =
+      m4.perspective(fieldOfViewRadians, aspect, 1, 2000);
+
+  // Вычисляем матрицу камеры, используя look at.
+  const cameraPosition = [settings.cameraX, settings.cameraY, 7];
+  const target = [0, 0, 0];
+  const up = [0, 1, 0];
+  const cameraMatrix = m4.lookAt(cameraPosition, target, up);
+
+-  drawScene(projectionMatrix, cameraMatrix, textureMatrix); 
++  drawScene(projectionMatrix, cameraMatrix, textureMatrix, textureProgramInfo); 
+}
+```
+
+Обратите внимание, что я переименовал `textureWorldMatrix` в `lightWorldMatrix` и
+`textureProjectionMatrix` в `lightProjectionMatrix`. Они действительно
+одно и то же, но раньше мы проецировали текстуру через произвольное пространство.
+Теперь мы пытаемся проецировать карту теней от источника света. Математика та же,
+но казалось уместным переименовать переменные.
+
+Выше мы сначала рендерим сферу и плоскость в текстуру глубины,
+используя цветной шейдер, который мы сделали для рисования линий усеченной пирамиды. Этот шейдер
+просто рисует сплошной цвет и ничего больше особенного не делает, что все,
+что нам нужно при рендеринге в текстуру глубины.
+
+После этого мы рендерим сцену снова на canvas, как мы делали раньше,
+проецируя текстуру в сцену.
+Когда мы ссылаемся на текстуру глубины в шейдере, только красное
+значение действительно, поэтому мы просто повторим его для красного, зеленого и синего.
+
+```glsl
+void main() {
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
+  bool inRange = 
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+-  vec4 projectedTexColor = texture2D(u_projectedTexture, projectedTexcoord.xy);
++  // канал 'r' имеет значения глубины
++  vec4 projectedTexColor = vec4(texture2D(u_projectedTexture, projectedTexcoord.xy).rrr, 1);
+  vec4 texColor = texture2D(u_texture, v_texcoord) * u_colorMult;
+  float projectedAmount = inRange ? 1.0 : 0.0;
+  gl_FragColor = mix(texColor, projectedTexColor, projectedAmount);
+}
+```
+
+Пока мы этим занимаемся, давайте добавим куб в сцену
+
+```js
++const cubeBufferInfo = twgl.primitives.createCubeBufferInfo(
++    gl,
++    2,  // размер
++);
+
+...
+
++const cubeUniforms = {
++  u_colorMult: [0.5, 1, 0.5, 1],  // светло-зеленый
++  u_color: [0, 0, 1, 1],
++  u_texture: checkerboardTexture,
++  u_world: m4.translation(3, 1, 0),
++};
+
+...
+
+function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo) {
+
+    ...
+
++    // ------ Рисуем куб --------
++
++    // Настраиваем все необходимые атрибуты.
++    gl.bindVertexArray(cubeVAO);
++
++    // Устанавливаем uniforms, которые мы только что вычислили
++    twgl.setUniforms(programInfo, cubeUniforms);
++
++    // вызывает gl.drawArrays или gl.drawElements
++    twgl.drawBufferInfo(gl, cubeBufferInfo);
+
+...
+```
+
+и давайте настроим настройки. Мы переместим камеру
+и расширим поле зрения для проекции текстуры, чтобы покрыть больше сцены
+
+```js
+const settings = {
+-  cameraX: 2.5,
++  cameraX: 6,
+  cameraY: 5,
+  posX: 2.5,
+  posY: 4.8,
+  posZ: 4.3,
+  targetX: 2.5,
+  targetY: 0,
+  targetZ: 3.5,
+  projWidth: 1,
+  projHeight: 1,
+  perspective: true,
+-  fieldOfView: 45,
++  fieldOfView: 120,
+};
+```
+
+примечание: Я переместил код, который рисует куб линий, показывающий
+усеченную пирамиду, за пределы функции `drawScene`.
+
+{{{example url="../webgl-shadows-depth-texture.html"}}}
+
+Это точно то же самое, что и верхний пример, за исключением того, что вместо
+загрузки изображения мы генерируем текстуру глубины,
+рендеря сцену в нее. Если вы хотите проверить, настройте `cameraX`
+обратно на 2.5 и `fieldOfView` на 45, и это должно выглядеть так же,
+как выше, за исключением того, что наша новая текстура глубины проецируется
+вместо загруженного изображения.
+
+Значения глубины идут от 0.0 до 1.0, представляя их позицию
+через усеченную пирамиду, поэтому 0.0 (темный) близко к кончику
+усеченной пирамиды, а 1.0 (светлый) на дальнем открытом конце.
+
+Итак, все, что осталось сделать, это вместо выбора между нашим проецируемым
+цветом текстуры и нашим маппированным цветом текстуры, мы можем использовать глубину из
+текстуры глубины, чтобы проверить, является ли Z позиция из текстуры глубины
+ближе или дальше от источника света, чем глубина пикселя, который мы
+просим нарисовать. Если глубина из текстуры глубины ближе, то что-то
+блокировало свет, и этот пиксель в тени.
+
+```glsl
+void main() {
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
++  float currentDepth = projectedTexcoord.z;
+
+  bool inRange = 
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+-  vec4 projectedTexColor = vec4(texture(u_projectedTexture, projectedTexcoord.xy).rrr, 1);
++  float projectedDepth = texture(u_projectedTexture, projectedTexcoord.xy).r;
++  float shadowLight = (inRange && projectedDepth <= currentDepth) ? 0.0 : 1.0;  
+
+  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;
+-  outColor = mix(texColor, projectedTexColor, projectedAmount);
++  outColor = vec4(texColor.rgb * shadowLight, texColor.a);
+}
+```
+
+Выше, если `projectedDepth` меньше, чем `currentDepth`, то
+с точки зрения источника света что-то было ближе к
+источнику света, поэтому этот пиксель, который мы собираемся нарисовать, в тени.
+
+Если мы запустим это, мы получим тень
+
+{{{example url="../webgl-shadows-basic.html" }}}
+
+Это как-то работает, мы можем видеть тень сферы на
+земле, но что с этими странными узорами там, где
+не должно быть тени? Эти узоры
+называются *shadow acne*. Они происходят из того факта, что
+данные глубины, сохраненные в текстуре глубины, были квантованы как в том,
+что это текстура, сетка пикселей, она была спроецирована с
+точки зрения источника света, но мы сравниваем ее со значениями с точки зрения камеры. Это означает, что сетка значений в
+карте глубины не выровнена с нашей камерой, и
+поэтому, когда мы вычисляем `currentDepth`, бывают времена, когда одно значение
+будет немного больше или немного меньше, чем `projectedDepth`.
+
+Давайте добавим смещение.
+
+```glsl
+...
+
++uniform float u_bias;
+
+void main() {
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
+-  float currentDepth = projectedTexcoord.z;
++  float currentDepth = projectedTexcoord.z + u_bias;
+
+  bool inRange = 
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+  float projectedDepth = texture(u_projectedTexture, projectedTexcoord.xy).r;
+  float shadowLight = (inRange && projectedDepth <= currentDepth) ? 0.0 : 1.0;  
+
+  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;
+  outColor = vec4(texColor.rgb * shadowLight, texColor.a);
+}
+```
+
+И нам нужно установить его
+
+```js
+const settings = {
+  cameraX: 2.75,
+  cameraY: 5,
+  posX: 2.5,
+  posY: 4.8,
+  posZ: 4.3,
+  targetX: 2.5,
+  targetY: 0,
+  targetZ: 3.5,
+  projWidth: 1,
+  projHeight: 1,
+  perspective: true,
+  fieldOfView: 120,
++  bias: -0.006,
+};
+
+...
+
+function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo, /**/u_lightWorldPosition) {
+  // Создаем матрицу вида из матрицы камеры.
+  const viewMatrix = m4.inverse(cameraMatrix);
+
+  gl.useProgram(programInfo.program);
+
+  // устанавливаем uniforms, которые одинаковы для сферы и плоскости
+  // примечание: любые значения без соответствующего uniform в шейдере
+  // игнорируются.
+  twgl.setUniforms(programInfo, {
+    u_view: viewMatrix,
+    u_projection: projectionMatrix,
++    u_bias: settings.bias,
+    u_textureMatrix: textureMatrix,
+    u_projectedTexture: depthTexture,
+  });
+
+  ...
+```
+
+{{{example url="../webgl-shadows-basic-w-bias.html"}}}
+
+сдвиньте значение bias, и вы можете увидеть, как это влияет на то, когда и где
+появляются узоры.
+
+Чтобы приблизиться к завершению, давайте фактически добавим расчет прожекторного освещения
+из [статьи о прожекторном освещении](webgl-3d-lighting-spot.html).
+
+Сначала давайте вставим нужные части в вершинный шейдер напрямую
+из [той статьи](webgl-3d-lighting-spot.html).
+
+```glsl
+#version 300 es
+in vec4 a_position;
+in vec2 a_texcoord;
++in vec3 a_normal;
+
++uniform vec3 u_lightWorldPosition;
++uniform vec3 u_viewWorldPosition;
+
+uniform mat4 u_projection;
+uniform mat4 u_view;
+uniform mat4 u_world;
+uniform mat4 u_textureMatrix;
+
+out vec2 v_texcoord;
+out vec4 v_projectedTexcoord;
++out vec3 v_normal;
+
++out vec3 v_surfaceToLight;
++out vec3 v_surfaceToView;
+
+void main() {
+  // Умножаем позицию на матрицу.
+  vec4 worldPosition = u_world * a_position;
+
+  gl_Position = u_projection * u_view * worldPosition;
+
+  // Передаем координату текстуры в фрагментный шейдер.
+  v_texcoord = a_texcoord;
+
+  v_projectedTexcoord = u_textureMatrix * worldPosition;
+
++  // ориентируем нормали и передаем в фрагментный шейдер
++  v_normal = mat3(u_world) * a_normal;
++
++  // вычисляем мировую позицию поверхности
++  vec3 surfaceWorldPosition = (u_world * a_position).xyz;
++
++  // вычисляем вектор поверхности к источнику света
++  // и передаем его в фрагментный шейдер
++  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
++
++  // вычисляем вектор поверхности к виду/камере
++  // и передаем его в фрагментный шейдер
++  v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
+}
+```
+
+Затем фрагментный шейдер
+
+```glsl
+#version 300 es
+precision highp float;
+
+// Передается из вершинного шейдера.
+in vec2 v_texcoord;
+in vec4 v_projectedTexcoord;
++in vec3 v_normal;
++in vec3 v_surfaceToLight;
++in vec3 v_surfaceToView;
+
+uniform vec4 u_colorMult;
+uniform sampler2D u_texture;
+uniform sampler2D u_projectedTexture;
+uniform float u_bias;
++uniform float u_shininess;
++uniform vec3 u_lightDirection;
++uniform float u_innerLimit;          // в пространстве скалярного произведения
++uniform float u_outerLimit;          // в пространстве скалярного произведения
+
+out vec4 outColor;
+
+void main() {
++  // поскольку v_normal является varying, он интерполируется
++  // поэтому он не будет единичным вектором. Нормализация
++  // сделает его снова единичным вектором
++  vec3 normal = normalize(v_normal);
++
++  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
++  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
++  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
++
++  float dotFromDirection = dot(surfaceToLightDirection,
++                               -u_lightDirection);
++  float limitRange = u_innerLimit - u_outerLimit;
++  float inLight = clamp((dotFromDirection - u_outerLimit) / limitRange, 0.0, 1.0);
++  float light = inLight * dot(normal, surfaceToLightDirection);
++  float specular = inLight * pow(dot(normal, halfVector), u_shininess);
+
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
+  float currentDepth = projectedTexcoord.z + u_bias;
+
+  bool inRange =
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+  // канал 'r' имеет значения глубины
+  float projectedDepth = texture(u_projectedTexture, projectedTexcoord.xy).r;
+  float shadowLight = (inRange && projectedDepth <= currentDepth) ? 0.0 : 1.0;
+
+  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;
+-  outColor = vec4(texColor.rgb * shadowLight, texColor.a);
++  outColor = vec4(
++      texColor.rgb * light * shadowLight +
++      specular * shadowLight,
++      texColor.a);
+}
+```
+
+Обратите внимание, что мы просто используем `shadowLight` для корректировки эффекта `light` и
+`specular`. Если объект в тени, то света нет.
+
+Нам просто нужно установить uniforms
+
+```js
+-function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo) {
++function drawScene(
++    projectionMatrix,
++    cameraMatrix,
++    textureMatrix,
++    lightWorldMatrix,
++    programInfo) {
+  // Создаем матрицу вида из матрицы камеры.
+  const viewMatrix = m4.inverse(cameraMatrix);
+
+  gl.useProgram(programInfo.program);
+
+  // устанавливаем uniforms, которые одинаковы для сферы и плоскости
+  // примечание: любые значения без соответствующего uniform в шейдере
+  // игнорируются.
+  twgl.setUniforms(programInfo, {
+    u_view: viewMatrix,
+    u_projection: projectionMatrix,
+    u_bias: settings.bias,
+    u_textureMatrix: textureMatrix,
+    u_projectedTexture: depthTexture,
++    u_shininess: 150,
++    u_innerLimit: Math.cos(degToRad(settings.fieldOfView / 2 - 10)),
++    u_outerLimit: Math.cos(degToRad(settings.fieldOfView / 2)),
++    u_lightDirection: lightWorldMatrix.slice(8, 11).map(v => -v),
++    u_lightWorldPosition: lightWorldMatrix.slice(12, 15),
++    u_viewWorldPosition: cameraMatrix.slice(12, 15),
+  });
+
+...
+
+function render() {
+  ...
+
+-  drawScene(lightProjectionMatrix, lightWorldMatrix, m4.identity(), colorProgramInfo);
++  drawScene(
++      lightProjectionMatrix,
++      lightWorldMatrix,
++      m4.identity(),
++      lightWorldMatrix,
++      colorProgramInfo);
+
+  ...
+
+-  drawScene(projectionMatrix, cameraMatrix, textureMatrix, textureProgramInfo);
++  drawScene(
++      projectionMatrix,
++      cameraMatrix,
++      textureMatrix,
++      lightWorldMatrix,
++      textureProgramInfo);
+
+  ...
+}
+```
+
+Чтобы пройтись по нескольким из этих настроек uniform. Напомним из [статьи о прожекторном освещении](webgl-3d-lighting-spot.html),
+что настройки innerLimit и outerLimit находятся в пространстве скалярного произведения (пространство косинуса) и что
+нам нужна только половина поля зрения, поскольку они простираются вокруг направления света.
+Также напомним из [статьи о камере](webgl-3d-camera.html), что 3-я строка матрицы 4x4
+является осью Z, поэтому извлечение первых 3 значений 3-й строки из `lightWorldMatrix`
+дает нам направление -Z света. Мы хотим положительное направление, поэтому переворачиваем его.
+Аналогично та же статья говорит нам, что 4-я строка - это мировая позиция, поэтому мы можем получить
+lightWorldPosition и viewWorldPosition (также известную как мировая позиция камеры),
+извлекая их из их соответствующих матриц. Конечно, мы могли бы также
+получить их, раскрывая больше настроек или передавая больше переменных.
+
+Давайте также очистим фон до черного и установим линии усеченной пирамиды в белый
+
+```js
+function render() {
+
+  ...
+
+  // теперь рисуем сцену на canvas, проецируя текстуру глубины в сцену
+  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
++  gl.clearColor(0, 0, 0, 1);
+  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+  ...
+
+  // ------ Рисуем усеченную пирамиду ------
+  {
+
+    ...
+
+          // Устанавливаем uniforms, которые мы только что вычислили
+    twgl.setUniforms(colorProgramInfo, {
+-      u_color: [0, 0, 0, 1],
++      u_color: [1, 1, 1, 1],
+      u_view: viewMatrix,
+      u_projection: projectionMatrix,
+      u_world: mat,
+    });
+```
+
+И теперь у нас есть прожекторное освещение с тенями.
+
+{{{example url="../webgl-shadows-w-spot-light.html" }}}
+
+Для направленного света мы скопируем код шейдера из
+[статьи о направленном освещении](webgl-3d-lighting-directional.html)
+и изменим нашу проекцию с перспективной на ортографическую.
+
+Сначала вершинный шейдер
+
+```glsl
+#version 300 es
+in vec4 a_position;
+in vec2 a_texcoord;
++in vec3 a_normal;
+
+-uniform vec3 u_lightWorldPosition;
+-uniform vec3 u_viewWorldPosition;
+
+uniform mat4 u_projection;
+uniform mat4 u_view;
+uniform mat4 u_world;
+uniform mat4 u_textureMatrix;
+
+out vec2 v_texcoord;
+out vec4 v_projectedTexcoord;
+out vec3 v_normal;
+
+-out vec3 v_surfaceToLight;
+-out vec3 v_surfaceToView;
+
+void main() {
+  // Умножаем позицию на матрицу.
+  vec4 worldPosition = u_world * a_position;
+
+  gl_Position = u_projection * u_view * worldPosition;
+
+  // Передаем координату текстуры в фрагментный шейдер.
+  v_texcoord = a_texcoord;
+
+  v_projectedTexcoord = u_textureMatrix * worldPosition;
+
+  // ориентируем нормали и передаем в фрагментный шейдер
+  v_normal = mat3(u_world) * a_normal;
+
+-  // вычисляем мировую позицию поверхности
+-  vec3 surfaceWorldPosition = (u_world * a_position).xyz;
+-
+-  // вычисляем вектор поверхности к источнику света
+-  // и передаем его в фрагментный шейдер
+-  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
+-
+-  // вычисляем вектор поверхности к виду/камере
+-  // и передаем его в фрагментный шейдер
+-  v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
+}
+```
+
+Затем фрагментный шейдер
+
+```glsl
+#version 300 es
+precision highp float;
+
+// Передается из вершинного шейдера.
+in vec2 v_texcoord;
+in vec4 v_projectedTexcoord;
+in vec3 v_normal;
+-in vec3 v_surfaceToLight;
+-in vec3 v_surfaceToView;
+
+uniform vec4 u_colorMult;
+uniform sampler2D u_texture;
+uniform sampler2D u_projectedTexture;
+uniform float u_bias;
+-uniform float u_shininess;
+-uniform vec3 u_lightDirection;
+-uniform float u_innerLimit;          // в пространстве скалярного произведения
+-uniform float u_outerLimit;          // в пространстве скалярного произведения
++uniform vec3 u_reverseLightDirection;
+
+out vec4 outColor;
+
+void main() {
+  // поскольку v_normal является varying, он интерполируется
+  // поэтому он не будет единичным вектором. Нормализация
+  // сделает его снова единичным вектором
+  vec3 normal = normalize(v_normal);
+
++  float light = dot(normal, u_reverseLightDirection);
+
+-  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+-  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
+-  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
+-
+-  float dotFromDirection = dot(surfaceToLightDirection,
+-                               -u_lightDirection);
+-  float limitRange = u_innerLimit - u_outerLimit;
+-  float inLight = clamp((dotFromDirection - u_outerLimit) / limitRange, 0.0, 1.0);
+-  float light = inLight * dot(normal, surfaceToLightDirection);
+-  float specular = inLight * pow(dot(normal, halfVector), u_shininess);
+
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
+  float currentDepth = projectedTexcoord.z + u_bias;
+
+  bool inRange =
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+  // канал 'r' имеет значения глубины
+  float projectedDepth = texture(u_projectedTexture, projectedTexcoord.xy).r;
+  float shadowLight = (inRange && projectedDepth <= currentDepth) ? 0.0 : 1.0;
+
+  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;
+  outColor = vec4(
+-      texColor.rgb * light * shadowLight +
+-      specular * shadowLight,
++      texColor.rgb * light * shadowLight,
+      texColor.a);
+}
+```
+
+и uniforms
+
+```js
+  // устанавливаем uniforms, которые одинаковы для сферы и плоскости
+  // примечание: любые значения без соответствующего uniform в шейдере
+  // игнорируются.
+  twgl.setUniforms(programInfo, {
+    u_view: viewMatrix,
+    u_projection: projectionMatrix,
+    u_bias: settings.bias,
+    u_textureMatrix: textureMatrix,
+    u_projectedTexture: depthTexture,
+-    u_shininess: 150,
+-    u_innerLimit: Math.cos(degToRad(settings.fieldOfView / 2 - 10)),
+-    u_outerLimit: Math.cos(degToRad(settings.fieldOfView / 2)),
+-    u_lightDirection: lightWorldMatrix.slice(8, 11).map(v => -v),
+-    u_lightWorldPosition: lightWorldMatrix.slice(12, 15),
+-    u_viewWorldPosition: cameraMatrix.slice(12, 15),
++    u_reverseLightDirection: lightWorldMatrix.slice(8, 11),
+  });
+```
+
+Я настроил камеру, чтобы видеть больше сцены.
+
+{{{example url="../webgl-shadows-w-directional-light.html"}}}
+
+Это указывает на что-то, что должно быть очевидно из кода выше, но наша
+карта теней только такая большая, поэтому даже though вычисления направленного света
+имеют только направление, нет позиции для самого света, мы все еще
+должны выбрать позицию, чтобы решить область для вычисления и применения
+карты теней.
+
+Эта статья становится длинной, и есть еще много вещей для покрытия, связанных
+с тенями, поэтому мы оставим остальное для [следующей статьи](webgl-shadows-continued.html). 
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-skinning.md b/webgl/lessons/ru/webgl-skinning.md
index a6924be44..70475f668 100644
--- a/webgl/lessons/ru/webgl-skinning.md
+++ b/webgl/lessons/ru/webgl-skinning.md
@@ -195,4 +195,1019 @@ var uniforms = {
   view: m4.translation(-6, 0, 0),
   bones: boneArray,
   color: [1, 0, 0, 1],
-}; 
\ No newline at end of file
+}; 
+
+Мы можем создать представления в boneArray, одно для каждой матрицы
+
+```
+// создаем представления для каждой кости. Это позволяет всем костям
+// существовать в 1 массиве для загрузки, но как отдельные
+// массивы для использования с математическими функциями
+const boneMatrices = [];  // данные uniform
+const bones = [];         // значение до умножения на обратную привязочную матрицу
+const bindPose = [];      // привязочная матрица
+for (let i = 0; i < numBones; ++i) {
+  boneMatrices.push(new Float32Array(boneArray.buffer, i * 4 * 16, 16));
+  bindPose.push(m4.identity());  // просто выделяем память
+  bones.push(m4.identity());     // просто выделяем память
+}
+```
+
+И затем некоторый код для манипуляции с матрицами костей. Мы просто будем вращать
+их в иерархии, как кости пальца.
+
+```
+// вращаем каждую кость на угол и симулируем иерархию
+function computeBoneMatrices(bones, angle) {
+  const m = m4.identity();
+  m4.zRotate(m, angle, bones[0]);
+  m4.translate(bones[0], 4, 0, 0, m);
+  m4.zRotate(m, angle, bones[1]);
+  m4.translate(bones[1], 4, 0, 0, m);
+  m4.zRotate(m, angle, bones[2]);
+  // bones[3] не используется
+}
+```
+
+Теперь вызовем это один раз, чтобы сгенерировать их начальные позиции, и используем результат
+для вычисления обратных привязочных матриц.
+
+```
+// вычисляем начальные позиции каждой матрицы
+computeBoneMatrices(bindPose, 0);
+
+// вычисляем их обратные
+const bindPoseInv = bindPose.map(function(m) {
+  return m4.inverse(m);
+});
+```
+
+Теперь мы готовы к рендерингу
+
+Сначала мы анимируем кости, вычисляя новую мировую матрицу для каждой
+
+```
+const t = time * 0.001;
+const angle = Math.sin(t) * 0.8;
+computeBoneMatrices(bones, angle);
+```
+
+Затем мы умножаем результат каждой на обратную привязочную позу, чтобы решить
+проблему, упомянутую выше
+
+```
+// умножаем каждую на ее bindPoseInverse
+bones.forEach((bone, ndx) => {
+  m4.multiply(bone, bindPoseInv[ndx], boneMatrices[ndx]);
+});
+```
+
+Затем все обычные вещи: настройка атрибутов, установка uniform значений и отрисовка.
+
+```
+gl.useProgram(programInfo.program);
+
+gl.bindVertexArray(skinVAO);
+
+// вызывает gl.uniformXXX, gl.activeTexture, gl.bindTexture
+twgl.setUniforms(programInfo, uniforms);
+
+// вызывает gl.drawArrays или gl.drawIndices
+twgl.drawBufferInfo(gl, bufferInfo, gl.LINES);
+```
+
+И вот результат
+
+{{{example url="../webgl-skinning.html" }}}
+
+Красные линии - это *скинированный* меш. Зеленые и синие линии представляют
+x-ось и y-ось каждой кости или "сустава". Вы можете видеть, как вершины,
+которые находятся под влиянием множественных костей, перемещаются между костями, которые влияют на них.
+Мы не покрыли, как рисуются кости, так как это не важно для объяснения того, как работает скининг.
+Смотрите код, если вам любопытно.
+
+ПРИМЕЧАНИЕ: кости против суставов сбивает с толку. Есть только 1 вещь, *матрицы*.
+Но в 3D пакете моделирования они обычно рисуют gizmo (UI виджет)
+между каждой матрицей. Это выглядит как кость. Суставы
+- это где находятся матрицы, и они рисуют линию или конус от каждого сустава
+к следующему, чтобы это выглядело как скелет.
+
+
+
+Одна вещь, которую стоит отметить, что мы, возможно, не делали раньше, мы создали атрибут `uvec4`, который является атрибутом, который получает беззнаковые целые числа. Если бы мы не использовали twgl,
+нам пришлось бы вызвать `gl.vertexAttribIPointer` для его настройки вместо более
+обычного `gl.vertexAttibPointer`.
+
+К сожалению, есть ограничение на количество uniform значений, которые вы можете использовать в шейдере.
+Нижний предел в WebGL составляет 64 vec4, что составляет только 8 mat4, и вам, вероятно,
+нужны некоторые из этих uniform значений для других вещей, например, у нас есть `color`
+в фрагментном шейдере, и у нас есть `projection` и `view`, что означает, что если
+мы были на устройстве с лимитом 64 vec4, мы могли бы иметь только 5 костей! Проверяя
+[WebGLStats](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_UNIFORM_VECTORS)
+большинство устройств поддерживают 128 vec4, и 70% из них поддерживают 256 vec4, но с
+нашим примером выше это все еще только 13 костей и 29 костей соответственно. 13
+даже недостаточно для персонажа в стиле Virtua Fighter 1 начала 90-х, и 29 не
+близко к числу, используемому в большинстве современных игр.
+
+Несколько способов обойти это. Один - предварительно обработать модели офлайн и разбить их
+на несколько частей, каждая из которых использует не более N костей. Это довольно сложно
+и приносит свой собственный набор проблем.
+
+Другой - хранить матрицы костей в текстуре. Это важное напоминание
+о том, что текстуры - это не просто изображения, они эффективно являются 2D массивами данных с произвольным доступом,
+которые вы можете передать в шейдер, и вы можете использовать их для всех видов вещей,
+которые не просто чтение изображений для текстурирования.
+
+Давайте передадим наши матрицы в текстуре, чтобы обойти лимит uniform значений. Чтобы сделать это
+легко, мы будем использовать текстуры с плавающей точкой.
+
+Давайте обновим шейдер, чтобы получить матрицы из текстуры.
+Мы сделаем текстуру с одной матрицей на строку. Каждый пиксель текстуры
+имеет R, G, B и A, это 4 значения, поэтому нам нужно только 4 пикселя на матрицу,
+один пиксель для каждой строки матрицы.
+Текстуры обычно могут быть по крайней мере 2048 пикселей в определенном измерении, поэтому
+это даст нам место для по крайней мере 2048 матриц костей, что достаточно.
+
+```
+#version 300 es
+in vec4 a_position;
+in vec4 a_weight;
+in uvec4 a_boneNdx;
+
+uniform mat4 projection;
+uniform mat4 view;
+*uniform sampler2D boneMatrixTexture;
+
++mat4 getBoneMatrix(uint boneNdx) {
++  return mat4(
++    texelFetch(boneMatrixTexture, ivec2(0, boneNdx), 0),
++    texelFetch(boneMatrixTexture, ivec2(1, boneNdx), 0),
++    texelFetch(boneMatrixTexture, ivec2(2, boneNdx), 0),
++    texelFetch(boneMatrixTexture, ivec2(3, boneNdx), 0));
++}
+
+void main() {
+
+  gl_Position = projection * view *
+*                (getBoneMatrix(a_boneNdx[0]) * a_position * a_weight[0] +
+*                 getBoneMatrix(a_boneNdx[1]) * a_position * a_weight[1] +
+*                 getBoneMatrix(a_boneNdx[2]) * a_position * a_weight[2] +
+*                 getBoneMatrix(a_boneNdx[3]) * a_position * a_weight[3]);
+
+}
+```
+
+Обратите внимание, что мы используем `texelFetch` вместо `texture` для получения данных из
+текстуры. `texelFetch` извлекает один пиксель из текстуры.
+Он принимает как входные данные sampler, ivec2 с координатами x,y текстуры
+в пикселях, и уровень mip как в
+
+```
+vec4 data = texelFetch(sampler2D, ivec2(x, y), lod);
+```
+
+Теперь мы настроим текстуру, в которую можем поместить матрицы костей
+
+```
+// подготавливаем текстуру для матриц костей
+const boneMatrixTexture = gl.createTexture();
+gl.bindTexture(gl.TEXTURE_2D, boneMatrixTexture);
+// поскольку мы хотим использовать текстуру для чистых данных, мы отключаем
+// фильтрацию
+gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+```
+
+И мы передадим эту текстуру как uniform.
+
+```
+const uniforms = {
+  projection: m4.orthographic(-20, 20, -10, 10, -1, 1),
+  view: m4.translation(-6, 0, 0),
+*  boneMatrixTexture,
+  color: [1, 0, 0, 1],
+};
+```
+
+Затем единственное, что нам нужно изменить, это обновить текстуру с
+последними матрицами костей при рендеринге
+
+```
+// обновляем текстуру текущими матрицами
+gl.bindTexture(gl.TEXTURE_2D, boneMatrixTexture);
+gl.texImage2D(
+    gl.TEXTURE_2D,
+    0,          // уровень
+    gl.RGBA32F, // внутренний формат
+    4,          // ширина 4 пикселя, каждый пиксель имеет RGBA, поэтому 4 пикселя - это 16 значений
+    numBones,   // одна строка на кость
+    0,          // граница
+    gl.RGBA,    // формат
+    gl.FLOAT,   // тип
+    boneArray);
+```
+
+Результат тот же, но мы решили проблему, что недостаточно
+uniform значений для передачи матриц через uniform.
+
+{{{example url="../webgl-skinning-bone-matrices-in-texture.html" }}}
+
+Итак, это основы скиннинга. Не так сложно написать код для отображения
+скинированного меша. Более сложная часть - это фактически получение данных. Вам обычно нужно
+какое-то 3D программное обеспечение, такое как blender/maya/3d studio max, а затем либо написать
+свой собственный экспортер, либо найти экспортер и формат, который предоставит все необходимые данные. Вы увидите, когда мы пройдемся по этому, что в загрузке скина в 10 раз больше кода, чем в его отображении, и это не включает, вероятно, в 20-30 раз больше кода в экспортере для получения данных из программы 3D моделирования. Кстати, это одна из вещей, которую часто упускают люди, пишущие свой собственный 3D движок. Движок - это легкая часть 😜
+
+Будет много кода, поэтому давайте сначала попробуем просто отобразить не-скинированную модель.
+
+Давайте попробуем загрузить файл glTF. [glTF](https://www.khronos.org/gltf/) как бы разработан для WebGL. Поискав в сети, я нашел [этот файл кита-убийцы blender](https://www.blendswap.com/blends/view/65255) от [Junskie Pastilan](https://www.blendswap.com/user/pasilan)
+
+
+ +Есть 2 формата верхнего уровня для glTF. Формат `.gltf` - это JSON файл, который обычно ссылается на файл `.bin`, который является бинарным файлом, содержащим обычно только геометрию и возможно данные анимации. Другой формат - это `.glb`, который является бинарным форматом. Это в основном просто JSON и любые другие файлы, объединенные в один бинарный файл с коротким заголовком и секцией размера/типа между каждым +объединенным куском. Для JavaScript я думаю, что формат `.gltf` немного проще для начала, поэтому давайте попробуем загрузить его. + +Сначала [я скачал файл .blend](https://www.blendswap.com/blends/view/65255), установил [blender](https://blender.org), установил [экспортер gltf](https://github.com/KhronosGroup/glTF-Blender-IO), загрузил файл в blender и экспортировал. + +
+ +> Быстрая заметка: 3D программное обеспечение, такое как Blender, Maya, 3DSMax - это чрезвычайно сложное программное обеспечение с тысячами опций. Когда я впервые изучил 3DSMax в 1996 году, я проводил 2-3 часа в день, читая 1000+ страничное руководство и работая с учебниками около 3 недель. Я сделал что-то подобное, когда изучал Maya несколько лет спустя. Blender так же сложен, и более того, у него очень другой интерфейс от практически всего другого программного обеспечения. Это просто короткий способ сказать, что вы должны ожидать потратить значительное время на изучение любого 3D пакета, который решите использовать. + +После экспорта я загрузил файл .gltf в мой текстовый редактор и посмотрел вокруг. Я использовал [эту шпаргалку](https://www.khronos.org/files/gltf20-reference-guide.pdf), чтобы разобраться в формате. + +Я хочу сделать ясным, что код ниже не является идеальным загрузчиком glTF. Это просто достаточно кода, чтобы заставить кита отображаться. Я подозреваю, что если бы мы попробовали разные файлы, мы столкнулись бы с областями, которые нужно изменить. + +Первое, что нам нужно сделать, это загрузить файл. Чтобы сделать это проще, давайте используем [async/await](https://javascript.info/async-await) JavaScript. Сначала давайте напишем некоторый код для загрузки файла `.gltf` и любых файлов, на которые он ссылается. + +``` +async function loadGLTF(url) { + const gltf = await loadJSON(url); + + // загружаем все ссылающиеся файлы относительно файла gltf + const baseURL = new URL(url, location.href); + gltf.buffers = await Promise.all(gltf.buffers.map((buffer) => { + const url = new URL(buffer.uri, baseURL.href); + return loadBinary(url.href); + })); + + ... + +async function loadFile(url, typeFunc) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`could not load: ${url}`); + } + return await response[typeFunc](); +} + +async function loadBinary(url) { + return loadFile(url, 'arrayBuffer'); +} + +async function loadJSON(url) { + return loadFile(url, 'json'); +} +``` + +Теперь нам нужно пройтись по данным и соединить вещи. + +Сначала давайте обработаем то, что glTF считает мешем. Меш - это коллекция примитивов. Примитив - это эффективно буферы и атрибуты, необходимые для рендеринга чего-то. Давайте используем библиотеку [twgl](https://twgljs.org), которую мы покрыли в [меньше кода больше веселья](webgl-less-code-more-fun.html). Мы пройдемся по мешам и для каждого создадим `BufferInfo`, который мы можем передать в `twgl.createVAOFromBufferInfo`. Напомним, что `BufferInfo` - это эффективно просто информация об атрибутах, индексы, если они есть, и количество элементов для передачи в `gl.drawXXX`. Например, куб только с позициями и нормалями может иметь BufferInfo с такой структурой + +``` +const cubeBufferInfo = { + attribs: { + 'a_POSITION': { buffer: WebGLBuffer, type: gl.FLOAT, numComponents: 3, }, + 'a_NORMAL': { buffer: WebGLBuffer, type: gl.FLOAT, numComponents: 3, }, + }, + numElements: 24, + indices: WebGLBuffer, + elementType: gl.UNSIGNED_SHORT, +} +``` + +Итак, мы пройдемся по каждому примитиву и сгенерируем BufferInfo как этот. + +Примитивы имеют массив атрибутов, каждый атрибут ссылается на accessor. Accessor говорит, какой тип данных там есть, например `VEC3`/`gl.FLOAT` и ссылается на bufferView. BufferView указывает некоторое представление в буфер. Учитывая индекс accessor, мы можем написать некоторый код, который возвращает WebGLBuffer с загруженными данными, accessor и stride, указанный для bufferView. + +``` +// Учитывая индекс accessor, возвращаем accessor, WebGLBuffer и stride +function getAccessorAndWebGLBuffer(gl, gltf, accessorIndex) { + const accessor = gltf.accessors[accessorIndex]; + const bufferView = gltf.bufferViews[accessor.bufferView]; + if (!bufferView.webglBuffer) { + const buffer = gl.createBuffer(); + const target = bufferView.target || gl.ARRAY_BUFFER; + const arrayBuffer = gltf.buffers[bufferView.buffer]; + const data = new Uint8Array(arrayBuffer, bufferView.byteOffset, bufferView.byteLength); + gl.bindBuffer(target, buffer); + gl.bufferData(target, data, gl.STATIC_DRAW); + bufferView.webglBuffer = buffer; + } + return { + accessor, + buffer: bufferView.webglBuffer, + stride: bufferView.stride || 0, + }; +} +``` + +Нам также нужен способ конвертировать из типа glTF accessor в количество компонентов + +``` +function throwNoKey(key) { + throw new Error(`no key: ${key}`); +} + +const accessorTypeToNumComponentsMap = { + 'SCALAR': 1, + 'VEC2': 2, + 'VEC3': 3, + 'VEC4': 4, + 'MAT2': 4, + 'MAT3': 9, + 'MAT4': 16, +}; + +function accessorTypeToNumComponents(type) { + return accessorTypeToNumComponentsMap[type] || throwNoKey(type); +} +``` + +Теперь, когда мы создали эти функции, мы можем использовать их для настройки наших мешей + +Примечание: файлы glTF могут якобы определять материалы, но экспортер не поместил никаких материалов в файл, даже хотя экспорт материалов был отмечен. Я могу только догадываться, что экспортер не обрабатывает каждый вид материала в blender, что неудачно. Мы будем использовать материал по умолчанию, если в файле нет материала. Поскольку в этом файле нет материалов, здесь нет кода для использования материалов glTF. + +``` +const defaultMaterial = { + uniforms: { + u_diffuse: [.5, .8, 1, 1], + }, +}; + +// настраиваем меши +gltf.meshes.forEach((mesh) => { + mesh.primitives.forEach((primitive) => { + const attribs = {}; + let numElements; + for (const [attribName, index] of Object.entries(primitive.attributes)) { + const {accessor, buffer, stride} = getAccessorAndWebGLBuffer(gl, gltf, index); + numElements = accessor.count; + attribs[`a_${attribName}`] = { + buffer, + type: accessor.componentType, + numComponents: accessorTypeToNumComponents(accessor.type), + stride, + offset: accessor.byteOffset | 0, + }; + } + + const bufferInfo = { + attribs, + numElements, + }; + + if (primitive.indices !== undefined) { + const {accessor, buffer} = getAccessorAndWebGLBuffer(gl, gltf, primitive.indices); + bufferInfo.numElements = accessor.count; + bufferInfo.indices = buffer; + bufferInfo.elementType = accessor.componentType; + } + + primitive.bufferInfo = bufferInfo; + + // создаем VAO для этого примитива + primitive.vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, primitive.bufferInfo); + + // сохраняем информацию о материале для этого примитива + primitive.material = gltf.materials && gltf.materials[primitive.material] || defaultMaterial; + }); +}); +``` + +Теперь каждый примитив будет иметь свойство `bufferInfo` и `material`. + +Для скиннинга нам почти всегда нужен какой-то граф сцены. Мы создали граф сцены в [статье о графах сцены](webgl-scene-graph.html), поэтому давайте используем тот. + +``` +class TRS { + constructor(position = [0, 0, 0], rotation = [0, 0, 0, 1], scale = [1, 1, 1]) { + this.position = position; + this.rotation = rotation; + this.scale = scale; + } + getMatrix(dst) { + dst = dst || new Float32Array(16); + m4.compose(this.position, this.rotation, this.scale, dst); + return dst; + } +} + +class Node { + constructor(source, name) { + this.name = name; + this.source = source; + this.parent = null; + this.children = []; + this.localMatrix = m4.identity(); + this.worldMatrix = m4.identity(); + this.drawables = []; + } + setParent(parent) { + if (this.parent) { + this.parent._removeChild(this); + this.parent = null; + } + if (parent) { + parent._addChild(this); + this.parent = parent; + } + } + updateWorldMatrix(parentWorldMatrix) { + const source = this.source; + if (source) { + source.getMatrix(this.localMatrix); + } + + if (parentWorldMatrix) { + // была передана матрица, поэтому делаем математику + m4.multiply(parentWorldMatrix, this.localMatrix, this.worldMatrix); + } else { + // матрица не была передана, поэтому просто копируем локальную в мировую + m4.copy(this.localMatrix, this.worldMatrix); + } + + // теперь обрабатываем всех детей + const worldMatrix = this.worldMatrix; + for (const child of this.children) { + child.updateWorldMatrix(worldMatrix); + } + } + traverse(fn) { + fn(this); + for (const child of this.children) { + child.traverse(fn); + } + } + _addChild(child) { + this.children.push(child); + } + _removeChild(child) { + const ndx = this.children.indexOf(child); + this.children.splice(ndx, 1); + } +} +``` + +Есть несколько заметных изменений от кода в [статье о графе сцены](webgl-scene-graph.html). + +* Этот код использует функцию `class` из ES6. + + Гораздо приятнее использовать синтаксис `class`, чем старый стиль определения класса. + +* Мы добавили массив drawables в `Node` + + Это будет список вещей для отрисовки из этого Node. Мы поместим + экземпляры класса в этот список, которые отвечают за выполнение + фактической отрисовки. Таким образом, мы можем универсально рисовать разные вещи, + используя разные классы. + + Примечание: Не ясно для меня, что помещение массива drawables в Node + является лучшим решением. Я чувствую, что сам граф сцены, возможно, + вообще не должен содержать drawables. Вещи, которые нужно рисовать, могли бы вместо этого + просто ссылаться на узел в графе, где получить их данные. + Этот способ с drawables в графе распространен, поэтому давайте начнем с этого. + +* Мы добавили метод `traverse`. + + Он вызывает функцию, передавая ей текущий узел, а затем рекурсивно делает то же + самое для всех дочерних узлов. + +* Класс `TRS` использует кватернион для вращения + + Мы не покрыли кватернионы, и, честно говоря, я не думаю, что понимаю их + достаточно хорошо, чтобы объяснить их. К счастью, нам не нужно знать, как они + работают, чтобы использовать их. Мы просто берем данные из файла gltf и вызываем + функцию, которая строит матрицу из этих данных, и используем матрицу. + +Узлы в файле glTF хранятся как плоский массив. +Мы конвертируем данные узлов в glTF в экземпляры `Node`. Мы сохраняем старый массив +данных узлов как `origNodes`, так как он понадобится нам позже. + +``` +const origNodes = gltf.nodes; +gltf.nodes = gltf.nodes.map((n) => { + const {name, skin, mesh, translation, rotation, scale} = n; + const trs = new TRS(translation, rotation, scale); + const node = new Node(trs, name); + const realMesh = gltf.meshes[mesh]; + if (realMesh) { + node.drawables.push(new MeshRenderer(realMesh)); + } + return node; +}); +``` + +Выше мы создали экземпляр `TRS` для каждого узла, экземпляр `Node` для каждого узла, и, если было свойство `mesh`, мы искали данные меша, которые мы настроили раньше, и создавали `MeshRenderer` для его отрисовки. + +Давайте создадим `MeshRenderer`. Это просто инкапсуляция кода, который мы использовали в [меньше кода больше веселья](webgl-less-code-more-fun.html) для рендеринга одной модели. Все, что он делает, это держит ссылку на меш, а затем для каждого примитива настраивает программу, атрибуты и uniform значения и в конечном итоге вызывает `gl.drawArrays` или `gl.drawElements` через `twgl.drawBufferInfo`; + +``` +class MeshRenderer { + constructor(mesh) { + this.mesh = mesh; + } + render(node, projection, view, sharedUniforms) { + const {mesh} = this; + gl.useProgram(meshProgramInfo.program); + for (const primitive of mesh.primitives) { + gl.bindVertexArray(primitive.vao); + twgl.setUniforms(meshProgramInfo, { + u_projection: projection, + u_view: view, + u_world: node.worldMatrix, + }, primitive.material.uniforms, sharedUniforms); + twgl.drawBufferInfo(gl, primitive.bufferInfo); + } + } +} +``` + +Мы создали узлы, теперь нам нужно фактически расположить их в графе сцены. Это делается на 2 уровнях в glTF. +Сначала каждый узел имеет необязательный массив детей, которые также являются индексами в массив узлов, поэтому мы можем пройти все +узлы и родить их детей + +``` +function addChildren(nodes, node, childIndices) { + childIndices.forEach((childNdx) => { + const child = nodes[childNdx]; + child.setParent(node); + }); +} + +// располагаем узлы в граф +gltf.nodes.forEach((node, ndx) => { + const children = origNodes[ndx].children; + if (children) { + addChildren(gltf.nodes, node, children); + } +}); +``` + +Затем есть массив сцен. Сцена ссылается на +массив узлов по индексу в массив узлов, которые находятся внизу сцены. Не ясно для меня, почему они не просто начали с одного корневого узла, но что бы то ни было, это то, что в файле glTF, поэтому мы создаем корневой узел и родим всех детей сцены к этому узлу + +``` + // настраиваем сцены + for (const scene of gltf.scenes) { + scene.root = new Node(new TRS(), scene.name); + addChildren(gltf.nodes, scene.root, scene.nodes); + } + + return gltf; +} +``` + +и мы закончили с загрузкой, по крайней мере, только мешей. Давайте +отметим основную функцию как `async`, чтобы мы могли использовать ключевое слово `await`. + +``` +async function main() { +``` + +и мы можем загрузить файл gltf так + +``` +const gltf = await loadGLTF('resources/models/killer_whale/whale.CYCLES.gltf'); +``` + +Для рендеринга нам нужен шейдер, который соответствует данным в файле gltf. Давайте посмотрим на данные в файле gltf для примитива, который в нем есть + +``` +{ + "name" : "orca", + "primitives" : [ + { + "attributes" : { + "JOINTS_0" : 5, + "NORMAL" : 2, + "POSITION" : 1, + "TANGENT" : 3, + "TEXCOORD_0" : 4, + "WEIGHTS_0" : 6 + }, + "indices" : 0 + } + ] +} +``` + +Глядя на это, для рендеринга давайте просто используем `NORMAL` и `POSITION`. Мы добавили `a_` в начало каждого атрибута, поэтому вершинный шейдер, подобный этому, должен работать + +``` +#version 300 es +in vec4 a_POSITION; +in vec3 a_NORMAL; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; + +out vec3 v_normal; + +void main() { + gl_Position = u_projection * u_view * u_world * a_POSITION; + v_normal = mat3(u_world) * a_NORMAL; +} +``` + +и для фрагментного шейдера давайте используем простое направленное освещение + +``` +#version 300 es +precision highp float; + +int vec3 v_normal; + +uniform vec4 u_diffuse; +uniform vec3 u_lightDirection; + +out vec4 outColor; + +void main () { + vec3 normal = normalize(v_normal); + float light = dot(u_lightDirection, normal) * .5 + .5; + outColor = vec4(u_diffuse.rgb * light, u_diffuse.a); +} +``` + +Обратите внимание, что мы берем скалярное произведение, как мы покрыли в [статье о направленном освещении](webgl-3d-lighting-directional.html), но в отличие от того, здесь скалярное произведение умножается на .5 и мы добавляем .5. При нормальном направленном освещении поверхность освещена на 100%, когда она направлена прямо на свет, и затухает до 0%, когда поверхность перпендикулярна свету. Это означает, что вся 1/2 модели, обращенная от света, черная. Умножая на .5 и добавляя .5, мы берем скалярное произведение от -1 <-> 1 к 0 <-> 1, что означает, что оно будет черным только когда направлено в полную противоположную сторону. Это дает дешевое, но приятное освещение для простых тестов. + +Итак, нам нужно скомпилировать и связать шейдеры. + +``` +// компилирует и связывает шейдеры, ищет расположения атрибутов и uniform значений +const meshProgramInfo = twgl.createProgramInfo(gl, [meshVS, fs]); +``` + +и затем для рендеринга все, что отличается от раньше, это это + +``` +const sharedUniforms = { + u_lightDirection: m4.normalize([-1, 3, 5]), +}; + +function renderDrawables(node) { + for(const drawable of node.drawables) { + drawable.render(node, projection, view, sharedUniforms); + } +} + +for (const scene of gltf.scenes) { + // обновляем все мировые матрицы в сцене. + scene.root.updateWorldMatrix(); + // проходим сцену и рендерим все рендерируемые объекты + scene.root.traverse(renderDrawables); +} +``` + +Оставшееся от раньше (не показано выше) - это наш код для вычисления матрицы проекции, матрицы камеры и матрицы вида. Затем мы просто проходим каждую сцену, вызываем `scene.root.updateWorldMatrix`, который обновит мировую +матрицу всех узлов в том графе. Затем мы вызываем `scene.root.traverse` с `renderDrawables`. + +`renderDrawables` вызывает метод render всех drawables на том узле, передавая проекцию, вид и информацию об освещении через `sharedUniforms`. + +{{{example url="../webgl-skinning-3d-gltf.html" }}} + +Теперь, когда это работает, давайте обработаем скины. + +Сначала давайте создадим класс для представления скина. Он будет управлять списком суставов, что является другим словом для узлов в графе сцены, которые применяются к скину. Он также будет иметь обратные привязочные матрицы и будет управлять текстурой, в которую мы помещаем матрицы суставов. + +``` +class Skin { + constructor(joints, inverseBindMatrixData) { + this.joints = joints; + this.inverseBindMatrices = []; + this.jointMatrices = []; + // выделяем достаточно места для одной матрицы на сустав + this.jointData = new Float32Array(joints.length * 16); + // создаем представления для каждого сустава и inverseBindMatrix + for (let i = 0; i < joints.length; ++i) { + this.inverseBindMatrices.push(new Float32Array( + inverseBindMatrixData.buffer, + inverseBindMatrixData.byteOffset + Float32Array.BYTES_PER_ELEMENT * 16 * i, + 16)); + this.jointMatrices.push(new Float32Array( + this.jointData.buffer, + Float32Array.BYTES_PER_ELEMENT * 16 * i, + 16)); + } + // создаем текстуру для хранения матриц суставов + this.jointTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.jointTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } + update(node) { + const globalWorldInverse = m4.inverse(node.worldMatrix); + // проходим каждый сустав и получаем его текущую мировую матрицу + // применяем обратные привязочные матрицы и сохраняем + // весь результат в текстуре + for (let j = 0; j < this.joints.length; ++j) { + const joint = this.joints[j]; + const dst = this.jointMatrices[j]; + m4.multiply(globalWorldInverse, joint.worldMatrix, dst); + m4.multiply(dst, this.inverseBindMatrices[j], dst); + } + gl.bindTexture(gl.TEXTURE_2D, this.jointTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 4, this.joints.length, 0, + gl.RGBA, gl.FLOAT, this.jointData); + } +} +``` + +И как у нас был `MeshRenderer`, давайте создадим `SkinRenderer`, который использует `Skin` для рендеринга скинированного меша. + +``` +class SkinRenderer { + constructor(mesh, skin) { + this.mesh = mesh; + this.skin = skin; + } + render(node, projection, view, sharedUniforms) { + const {skin, mesh} = this; + skin.update(node); + gl.useProgram(skinProgramInfo.program); + for (const primitive of mesh.primitives) { + gl.bindVertexArray(primitive.vao); + twgl.setUniforms(skinProgramInfo, { + u_projection: projection, + u_view: view, + u_world: node.worldMatrix, + u_jointTexture: skin.jointTexture, + u_numJoints: skin.joints.length, + }, primitive.material.uniforms, sharedUniforms); + twgl.drawBufferInfo(gl, primitive.bufferInfo); + } + } +} +``` + +Вы можете видеть, что это очень похоже на `MeshRenderer`. У него есть ссылка на `Skin`, которую он использует для обновления всех матриц, необходимых для рендеринга. Затем он следует стандартному шаблону для рендеринга, используя программу, настраивая атрибуты, устанавливая все uniform значения с помощью `twgl.setUniforms`, который также привязывает текстуры, а затем рендерит. + +Нам также нужен вершинный шейдер, который поддерживает скининг + +``` +const skinVS = `#version 300 es +in vec4 a_POSITION; +in vec3 a_NORMAL; +in vec4 a_WEIGHTS_0; +in uvec4 a_JOINTS_0; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; +uniform sampler2D u_jointTexture; +uniform float u_numJoints; + +out vec3 v_normal; + +mat4 getBoneMatrix(uint jointNdx) { + return mat4( + texelFetch(u_jointTexture, ivec2(0, jointNdx), 0), + texelFetch(u_jointTexture, ivec2(1, jointNdx), 0), + texelFetch(u_jointTexture, ivec2(2, jointNdx), 0), + texelFetch(u_jointTexture, ivec2(3, jointNdx), 0)); +} + +void main() { + mat4 skinMatrix = getBoneMatrix(a_JOINTS_0[0]) * a_WEIGHTS_0[0] + + getBoneMatrix(a_JOINTS_0[1]) * a_WEIGHTS_0[1] + + getBoneMatrix(a_JOINTS_0[2]) * a_WEIGHTS_0[2] + + getBoneMatrix(a_JOINTS_0[3]) * a_WEIGHTS_0[3]; + mat4 world = u_world * skinMatrix; + gl_Position = u_projection * u_view * world * a_POSITION; + v_normal = mat3(world) * a_NORMAL; +} +`; +``` + +Это в значительной степени то же самое, что и наш скининг шейдер выше. Мы переименовали атрибуты, чтобы они соответствовали тому, что в файле gltf. + +Самое большое изменение - это создание `skinMatrix`. В нашем предыдущем скининг шейдере мы умножали позицию на каждую отдельную матрицу сустава/кости и умножали их на вес влияния для каждого сустава. В этом случае мы вместо этого складываем матрицы, умноженные на веса, и просто умножаем на позицию один раз. Это дает тот же результат, но мы можем использовать `skinMatrix` для умножения нормали, что нам нужно делать, иначе нормали не будут соответствовать скину. + +Также обратите внимание, что мы умножаем на матрицу `u_world` здесь. Мы вычли ее в `Skin.update` с этими строками + +``` +*const globalWorldInverse = m4.inverse(node.worldMatrix); +// проходим каждый сустав и получаем его текущую мировую матрицу +// применяем обратные привязочные матрицы и сохраняем +// весь результат в текстуре +for (let j = 0; j < this.joints.length; ++j) { + const joint = this.joints[j]; + const dst = this.jointMatrices[j]; +* m4.multiply(globalWorldInverse, joint.worldMatrix, dst); +``` + +Делаете ли вы это или нет - зависит от вас. Причина для этого в том, что это позволяет вам инстанцировать скин. Другими словами, вы можете рендерить скинированный меш в точно такой же позе в более чем +одном месте в том же кадре. Идея в том, что если есть много суставов, то выполнение всей матричной математики для скинированного меша медленно, поэтому +вы делаете эту математику один раз, а затем можете отобразить этот скинированный +меш в разных местах, просто перерендерив с другой мировой матрицей. + +Это может быть полезно для отображения толпы персонажей. К сожалению, все персонажи будут в точно такой же позе, поэтому не ясно для меня, действительно ли это так полезно или нет. Как часто такая ситуация действительно возникает? Вы можете убрать умножение на обратную мировую матрицу узла в `Skin` и убрать умножение на `u_world` в шейдере, и результат будет выглядеть так же, вы просто не можете *инстанцировать* этот скинированный меш. Конечно, вы можете рендерить тот же скинированный меш столько раз, сколько хотите, в разных позах. Вам понадобится другой объект `Skin`, указывающий на разные узлы, которые находятся в какой-то другой ориентации. + +Вернувшись к нашему коду загрузки, когда мы создаем экземпляры `Node`, если есть свойство `skin`, мы запомним его, чтобы мы могли создать `Skin` для него. + +``` ++const skinNodes = []; +const origNodes = gltf.nodes; +gltf.nodes = gltf.nodes.map((n) => { + const {name, skin, mesh, translation, rotation, scale} = n; + const trs = new TRS(translation, rotation, scale); + const node = new Node(trs, name); + const realMesh = gltf.meshes[mesh]; ++ if (skin !== undefined) { ++ skinNodes.push({node, mesh: realMesh, skinNdx: skin}); ++ } else if (realMesh) { + node.drawables.push(new MeshRenderer(realMesh)); + } + return node; +}); +``` + +После создания `Node` нам нужно создать `Skin`. Скины ссылаются на узлы через массив `joints`, который является списком индексов узлов, которые поставляют матрицы для суставов. +Скин также ссылается на accessor, который ссылается на обратные привязочные матрицы, сохраненные в файле. + +``` +// настраиваем скины +gltf.skins = gltf.skins.map((skin) => { + const joints = skin.joints.map(ndx => gltf.nodes[ndx]); + const {stride, array} = getAccessorTypedArrayAndStride(gl, gltf, skin.inverseBindMatrices); + return new Skin(joints, array); +}); +``` + +Код выше вызвал `getAccessorTypedArrayAndStride` с индексом accessor. Нам нужно предоставить этот код. Для данного accessor мы вернем представление [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) правильного типа, чтобы получить доступ к данным в буфере. + +``` +const glTypeToTypedArrayMap = { + '5120': Int8Array, // gl.BYTE + '5121': Uint8Array, // gl.UNSIGNED_BYTE + '5122': Int16Array, // gl.SHORT + '5123': Uint16Array, // gl.UNSIGNED_SHORT + '5124': Int32Array, // gl.INT + '5125': Uint32Array, // gl.UNSIGNED_INT + '5126': Float32Array, // gl.FLOAT +} + +// Учитывая GL тип, возвращаем нужный TypedArray +function glTypeToTypedArray(type) { + return glTypeToTypedArrayMap[type] || throwNoKey(type); +} + +// учитывая индекс accessor, возвращаем и accessor, и +// TypedArray для правильной части буфера +function getAccessorTypedArrayAndStride(gl, gltf, accessorIndex) { + const accessor = gltf.accessors[accessorIndex]; + const bufferView = gltf.bufferViews[accessor.bufferView]; + const TypedArray = glTypeToTypedArray(accessor.componentType); + const buffer = gltf.buffers[bufferView.buffer]; + return { + accessor, + array: new TypedArray( + buffer, + bufferView.byteOffset + (accessor.byteOffset || 0), + accessor.count * accessorTypeToNumComponents(accessor.type)), + stride: bufferView.byteStride || 0, + }; +} +``` + +Что-то стоит отметить о коде выше - мы создали таблицу с жестко закодированными константами WebGL. Это первый раз, когда мы это делаем. Константы не изменятся, поэтому это безопасно делать. + +Теперь, когда у нас есть скины, мы можем вернуться и добавить их к узлам, которые на них ссылались. + +``` +// Добавляем SkinRenderers к узлам со скинами +for (const {node, mesh, skinNdx} of skinNodes) { + node.drawables.push(new SkinRenderer(mesh, gltf.skins[skinNdx])); +} +``` + +Если бы мы рендерили так, мы могли бы не увидеть никакой разницы. Нам нужно анимировать некоторые из узлов. Давайте просто пройдемся по каждому узлу в `Skin`, другими словами, каждый сустав, и повернем его плюс минус немного на локальной X оси. + +Чтобы сделать это, мы сохраним оригинальную локальную матрицу для каждого сустава. Затем мы будем вращать эту оригинальную матрицу на некоторое количество каждый кадр, и используя специальную функцию, `m4.decompose`, мы конвертируем матрицу обратно в позицию, вращение, масштаб в сустав. + +``` +const origMatrix = new Map(); +function animSkin(skin, a) { + for(let i = 0; i < skin.joints.length; ++i) { + const joint = skin.joints[i]; + // если нет сохраненной матрицы для этого сустава + if (!origMatrix.has(joint)) { + // сохраняем матрицу для сустава + origMatrix.set(joint, joint.source.getMatrix()); + } + // получаем оригинальную матрицу + const origMatrix = origRotations.get(joint); + // вращаем ее + const m = m4.xRotate(origMatrix, a); + // разлагаем обратно в позицию, вращение, масштаб + // в сустав + m4.decompose(m, joint.source.position, joint.source.rotation, joint.source.scale); + } +} +``` + +и затем прямо перед рендерингом мы вызовем это + +``` +animSkin(gltf.skins[0], Math.sin(time) * .5); +``` + +Примечание: `animSkin` в основном хак. В идеале мы бы загрузили анимацию, которую создал какой-то художник, ИЛИ мы бы знали имена конкретных суставов, которыми мы хотим манипулировать в коде каким-то образом. В этом случае мы просто хотим увидеть, работает ли наш скининг, и это казалось самым легким способом сделать это. + +{{{example url="../webgl-skinning-3d-gltf-skinned.html" }}} + +Еще несколько заметок перед тем, как мы двинемся дальше + +Когда я впервые попытался заставить это работать, как и с большинством программ, вещи не появлялись на экране. + +Итак, первое, что я сделал, это пошел в конец скининг шейдера и добавил эту строку + +``` + gl_Position = u_projection * u_view * a_POSITION; +``` + +Во фрагментном шейдере я изменил его, чтобы просто рисовать сплошной цвет, добавив это в конце + +``` +outColor = vec4(1, 0, 0, 1); +``` + +Это убирает весь скининг и просто рисует меш в начале координат. Я настроил позицию камеры, пока у меня не было хорошего вида. + +``` +const cameraPosition = [5, 0, 5]; +const target = [0, 0, 0]; +``` + +Это показало силуэт кита-убийцы, поэтому я знал, что по крайней мере некоторые из данных работают. + +
+ +Затем я сделал фрагментный шейдер, показывающий нормали + +``` +outColor = vec4(normalize(v_normal) * .5 + .5, 1); +``` + +Нормали идут от -1 до 1, поэтому `* .5 + .5` корректирует их от 0 до 1 для просмотра как цветов. + +Вернувшись в вершинный шейдер, я просто передал нормаль через + +``` +v_normal = a_NORMAL; +``` + +Что дало мне вид, подобный этому + +
+ +Я не ожидал, что нормали будут плохими, но было хорошо начать с чего-то, что я ожидал, что будет работать, и подтвердить, что это действительно работает. + +Затем я подумал, что проверю веса. Все, что мне нужно было сделать, это +передать веса как нормали из вершинного шейдера + +``` +v_normal = a_WEIGHTS_0.xyz * 2. - 1.; +``` + +Веса идут от 0 до 1, но поскольку фрагментный шейдер ожидает нормали, я просто заставил веса идти от -1 до 1 + +Это изначально производило своего рода беспорядок цветов. Как только я выяснил ошибку, я получил изображение, подобное этому + +
+ +Не совсем очевидно, что это правильно, но это имеет смысл. Вы бы ожидали, что вершины, ближайшие к каждой кости, имеют сильный цвет, и вы бы ожидали увидеть кольца этого цвета в вершинах вокруг кости, поскольку веса в этой области, вероятно, 1.0 или по крайней мере все похожие. + +Поскольку оригинальное изображение было таким беспорядочным, я также попробовал отобразить индексы суставов с + +``` +v_normal = vec3(a_JOINTS_0.xyz) / float(textureSize(u_jointTexture, 0).y - 1) * 2. - 1.; +``` + +Индексы идут от 0 до numJoints - 1, поэтому код выше дал бы значения от -1 до 1. + +Как только вещи заработали, я получил изображение, подобное этому + +
+ +Снова это изначально было беспорядком цветов. Изображение выше - это то, как это выглядело после исправления. Это в значительной степени то, что вы бы ожидали увидеть для весов кита-убийцы. Кольца цвета вокруг каждой кости. + +Ошибка была связана с тем, как `twgl.createBufferInfoFromArrays`, который я использовал вместо twgl, когда я начал делать этот пример, выяснял количество компонентов. Были случаи, когда он игнорировал указанный, пытался угадать, и угадывал неправильно. Как только ошибка была исправлена, я убрал эти изменения из шейдеров. Обратите внимание, что я оставил их в коде выше закомментированными, если вы хотите поиграть с ними. + +Я хочу сделать ясным, что код выше предназначен для помощи в объяснении скиннинга. Он не предназначен для того, чтобы быть готовым к продакшену движком скиннинга. Я думаю, что если бы мы попытались сделать движок продакшен качества, мы столкнулись бы со многими вещами, которые мы, вероятно, захотели бы изменить, но я надеюсь, что прохождение через этот пример помогает немного демистифицировать скининг. +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-smallest-programs.md b/webgl/lessons/ru/webgl-smallest-programs.md index d6bae628b..57297813a 100644 --- a/webgl/lessons/ru/webgl-smallest-programs.md +++ b/webgl/lessons/ru/webgl-smallest-programs.md @@ -184,17 +184,118 @@ const [minSize, maxSize] = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE); // фрагментный шейдер precision highp float; -+uniform sampler tex; +uniform sampler tex; out vec4 outColor; void main() { -- outColor = vec4(1, 0, 0, 1); // красный -+ outColor = texture(tex, gl_PointCoord.xy); + outColor = texture(tex, gl_PointCoord.xy); } ``` Теперь, чтобы держать это простым, давайте сделаем текстуру с сырыми данными, как мы покрыли в [статье о текстурах данных](webgl-data-textures.html). -```js \ No newline at end of file +```js +// 2x2 пиксельные данные +const pixels = new Uint8Array([ + 0xFF, 0x00, 0x00, 0xFF, // красный + 0x00, 0xFF, 0x00, 0xFF, // зеленый + 0x00, 0x00, 0xFF, 0xFF, // синий + 0xFF, 0x00, 0xFF, 0xFF, // пурпурный +]); +const tex = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, tex); +gl.texImage2D( + gl.TEXTURE_2D, + 0, // уровень + gl.RGBA, // внутренний формат + 2, // ширина + 2, // высота + 0, // граница + gl.RGBA, // формат + gl.UNSIGNED_BYTE, // тип + pixels, // данные +); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +``` + +Поскольку WebGL по умолчанию использует текстуру 0 и поскольку uniforms +по умолчанию равны 0, больше ничего настраивать не нужно + +{{{example url="../webgl-simple-point-w-texture.html"}}} + +Это может быть отличным способом тестирования проблем, связанных с текстурами. +Мы все еще не используем буферы, атрибуты, и нам не пришлось +искать и устанавливать никаких uniforms. Например, если мы загрузили изображение, +оно не отображается. Что если мы попробуем шейдер выше, показывает ли он +изображение на точке? Мы рендерили в текстуру, а затем +хотим просмотреть текстуру. Обычно мы бы настроили некоторую геометрию +через буферы и атрибуты, но мы можем рендерить текстуру просто +показывая её на этой единственной точке. + +## Использование множественных одиночных `POINTS` + +Еще одно простое изменение к примеру выше. Мы можем изменить вершинный +шейдер на этот + +```glsl +#version 300 es +// вершинный шейдер + +in vec4 position; + +void main() { + gl_Position = position; + gl_PointSize = 120.0; +} +``` + +атрибуты имеют значение по умолчанию `0, 0, 0, 1`, поэтому с этим изменением +примеры выше все еще будут продолжать работать. Но теперь +мы получаем возможность установить позицию, если захотим. + +```js +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const positionLoc = gl.getAttribLocation(program, 'position'); +const colorLoc = gl.getUniformLocation(program, 'color'); +``` + +И использовать их + +``` +gl.useProgram(program); + +const numPoints = 5; +for (let i = 0; i < numPoints; ++i) { + const u = i / (numPoints - 1); // 0 до 1 + const clipspace = u * 1.6 - 0.8; // -0.8 до +0.8 + gl.vertexAttrib2f(positionLoc, clipspace, clipspace); + + gl.uniform4f(colorLoc, u, 0, 1 - u, 1); + + const offset = 0; + const count = 1; + gl.drawArrays(gl.POINTS, offset, count); +} +``` + +И теперь мы получаем 5 точек с 5 цветами +и мы все еще не должны были настраивать никакие буферы или +атрибуты. + +{{{example url="../webgl-simple-points.html"}}} + +Конечно, это **НЕ** способ, которым вы должны +рисовать много точек в WebGL. Если вы хотите рисовать много +точек, вы должны сделать что-то вроде настройки атрибута с позицией +для каждой точки и цветом для каждой точки и рисовать все точки +в одном вызове отрисовки. + +НО!, для тестирования, для отладки, для создания [MCVE](https://meta.stackoverflow.com/a/349790/128511) это отличный способ **минимизировать** +код. Как другой пример, допустим, мы рисуем в текстуры для постобработки +эффекта, и мы хотим их визуализировать. Мы могли бы просто нарисовать одну большую +точку для каждой, используя комбинацию этого примера и +предыдущего с текстурой. Никаких сложных шагов буферов +и атрибутов не нужно, отлично для отладки. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-glyphs.md b/webgl/lessons/ru/webgl-text-glyphs.md index c0167d9ff..f2e8e3874 100644 --- a/webgl/lessons/ru/webgl-text-glyphs.md +++ b/webgl/lessons/ru/webgl-text-glyphs.md @@ -198,4 +198,195 @@ image.src = "resources/8x8-font.png"; image.addEventListener('load', function() { // Теперь, когда изображение загружено, копируем его в текстуру. gl.bindTexture(gl.TEXTURE_2D, glyphTex); -} \ No newline at end of file + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +}); +``` + +Теперь, когда у нас есть текстура с глифами в ней, нам нужно её использовать. Для этого мы будем +строить вершины квадов на лету для каждого глифа. Эти вершины будут использовать координаты текстуры +для выбора конкретного глифа + +Учитывая строку, давайте построим вершины + +``` +function makeVerticesForString(fontInfo, s) { + var len = s.length; + var numVertices = len * 6; + var positions = new Float32Array(numVertices * 2); + var texcoords = new Float32Array(numVertices * 2); + var offset = 0; + var x = 0; + var maxX = fontInfo.textureWidth; + var maxY = fontInfo.textureHeight; + for (var ii = 0; ii < len; ++ii) { + var letter = s[ii]; + var glyphInfo = fontInfo.glyphInfos[letter]; + if (glyphInfo) { + var x2 = x + glyphInfo.width; + var u1 = glyphInfo.x / maxX; + var v1 = (glyphInfo.y + fontInfo.letterHeight - 1) / maxY; + var u2 = (glyphInfo.x + glyphInfo.width - 1) / maxX; + var v2 = glyphInfo.y / maxY; + + // 6 вершин на букву + positions[offset + 0] = x; + positions[offset + 1] = 0; + texcoords[offset + 0] = u1; + texcoords[offset + 1] = v1; + + positions[offset + 2] = x2; + positions[offset + 3] = 0; + texcoords[offset + 2] = u2; + texcoords[offset + 3] = v1; + + positions[offset + 4] = x; + positions[offset + 5] = fontInfo.letterHeight; + texcoords[offset + 4] = u1; + texcoords[offset + 5] = v2; + + positions[offset + 6] = x; + positions[offset + 7] = fontInfo.letterHeight; + texcoords[offset + 6] = u1; + texcoords[offset + 7] = v2; + + positions[offset + 8] = x2; + positions[offset + 9] = 0; + texcoords[offset + 8] = u2; + texcoords[offset + 9] = v1; + + positions[offset + 10] = x2; + positions[offset + 11] = fontInfo.letterHeight; + texcoords[offset + 10] = u2; + texcoords[offset + 11] = v2; + + x += glyphInfo.width + fontInfo.spacing; + offset += 12; + } else { + // у нас нет этого символа, поэтому просто продвигаемся + x += fontInfo.spaceWidth; + } + } + + // возвращаем ArrayBufferViews для части TypedArrays + // которые фактически использовались. + return { + arrays: { + position: new Float32Array(positions.buffer, 0, offset), + texcoord: new Float32Array(texcoords.buffer, 0, offset), + }, + numVertices: offset / 2, + }; +} +``` + +Чтобы использовать это, мы вручную создадим bufferInfo. ([См. предыдущую статью, если вы не помните, что такое bufferInfo](webgl-drawing-multiple-things.html)). + + // Вручную создаем bufferInfo + var textBufferInfo = { + attribs: { + a_position: { buffer: gl.createBuffer(), numComponents: 2, }, + a_texcoord: { buffer: gl.createBuffer(), numComponents: 2, }, + }, + numElements: 0, + }; + +И затем для рендеринга текста мы обновим буферы. Мы также сделаем текст динамическим + + textPositions.forEach(function(pos, ndx) { + + var name = names[ndx]; + + var s = name + ":" + pos[0].toFixed(0) + "," + pos[1].toFixed(0) + "," + pos[2].toFixed(0); + + var vertices = makeVerticesForString(fontInfo, s); + + + + // обновляем буферы + + textBufferInfo.attribs.a_position.numComponents = 2; + + gl.bindBuffer(gl.ARRAY_BUFFER, textBufferInfo.attribs.a_position.buffer); + + gl.bufferData(gl.ARRAY_BUFFER, vertices.arrays.position, gl.DYNAMIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, textBufferInfo.attribs.a_texcoord.buffer); + + gl.bufferData(gl.ARRAY_BUFFER, vertices.arrays.texcoord, gl.DYNAMIC_DRAW); + + // используем только позицию вида 'F' для текста + + // потому что pos в пространстве вида, это означает, что это вектор от глаза к + // некоторой позиции. Итак, перемещаемся вдоль этого вектора обратно к глазу на некоторое расстояние + var fromEye = m4.normalize(pos); + var amountToMoveTowardEye = 150; // потому что F 150 единиц длиной + var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; + var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; + var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; + var desiredTextScale = -1 / gl.canvas.height * 2; // 1x1 пиксели + var scale = viewZ * desiredTextScale; + + var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ); + textMatrix = m4.scale(textMatrix, scale, scale, 1); + + // настройка для рисования текста. + gl.useProgram(textProgramInfo.program); + + gl.bindVertexArray(textVAO); + + m4.copy(textMatrix, textUniforms.u_matrix); + webglUtils.setUniforms(textProgramInfo, textUniforms); + + // Рисуем текст. + gl.drawArrays(gl.TRIANGLES, 0, vertices.numVertices); + }); + +И вот это + +{{{example url="../webgl-text-glyphs-texture-atlas.html" }}} + +Это основная техника использования текстуры-атласа глифов. Есть несколько +очевидных вещей для добавления или способов улучшить это. + +* Переиспользовать те же массивы. + + В настоящее время `makeVerticesForString` выделяет новые Float32Arrays каждый раз, когда вызывается. + Это, вероятно, в конечном итоге вызовет икоту сборки мусора. Переиспользование + тех же массивов, вероятно, будет лучше. Вы бы увеличили массив, если он недостаточно большой, + и сохранили бы этот размер вокруг + +* Добавить поддержку возврата каретки + + Проверять на `\n` и переходить на новую строку при генерации вершин. Это сделало бы + легко создавать абзацы текста. + +* Добавить поддержку всех видов другого форматирования. + + Если вы хотите центрировать текст или выровнять его, вы могли бы добавить все это. + +* Добавить поддержку цветов вершин. + + Тогда вы могли бы окрашивать текст в разные цвета на букву. Конечно, вам пришлось бы + решить, как указать, когда менять цвета. + +* Рассмотреть генерацию текстуры-атласа глифов во время выполнения, используя 2D canvas + +Другая большая проблема, которую я не буду покрывать, заключается в том, что текстуры имеют ограниченный +размер, но шрифты эффективно неограниченны. Если вы хотите поддерживать весь Unicode +так, чтобы вы могли обрабатывать китайский и японский и арабский и все другие языки, +ну, по состоянию на 2015 год в Unicode более 110 000 глифов! Вы не можете поместить все +эти в текстуры. Просто недостаточно места. + +Способ, которым ОС и браузеры обрабатывают это, когда они ускорены GPU, заключается в использовании кэша текстуры глифов. Как +выше, они могут помещать текстуры в текстуру-атлас, но они, вероятно, делают область +для каждого глифа фиксированного размера. Они держат наиболее недавно использованные глифы в текстуре. +Если им нужно нарисовать глиф, которого нет в текстуре, они заменяют наименее +недавно использованный новым, который им нужен. Конечно, если этот глиф, который они +собираются заменить, все еще ссылается на квад, который еще не нарисован, то им нужно +рисовать с тем, что у них есть, прежде чем заменять глиф. + +Другая вещь, которую вы можете сделать, хотя я не рекомендую это, это объединить эту +технику с [предыдущей техникой](webgl-text-texture.html). Вы можете +рендерить глифы прямо в другую текстуру. + +Еще один способ рисовать текст в WebGL - это фактически использовать 3D текст. 'F' во +всех примерах выше - это 3D буква. Вы бы сделали одну для каждой буквы. 3D буквы +распространены для заголовков и логотипов фильмов, но не для многого другого. + +Я надеюсь, что это покрыло текст в WebGL. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-html.md b/webgl/lessons/ru/webgl-text-html.md index 64e594ee8..57f3a5015 100644 --- a/webgl/lessons/ru/webgl-text-html.md +++ b/webgl/lessons/ru/webgl-text-html.md @@ -221,4 +221,4 @@ TOC: Текст - HTML Это был просто метод, который я выбрал. Надеюсь, понятно, как использовать HTML для текста. [Далее мы -рассмотрим использование Canvas 2D для текста](webgl-text-canvas2d.html). \ No newline at end of file +покроем использование Canvas 2D для текста](webgl-text-canvas2d.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-texture.md b/webgl/lessons/ru/webgl-text-texture.md index bf1539004..0a912a2bb 100644 --- a/webgl/lessons/ru/webgl-text-texture.md +++ b/webgl/lessons/ru/webgl-text-texture.md @@ -265,23 +265,23 @@ viewProjectionMatrix, как в других примерах. Мы умножа {{{example url="../webgl-text-texture-separate-opaque-from-transparent.html" }}} Обратите внимание, мы не сортировали, как я упомянул выше. В данном случае, поскольку мы рисуем в основном непрозрачный текст, -вероятно, не будет заметной разницы, если мы отсортируем, поэтому я оставлю это для какой-то +вероятно, не будет заметной разницы, если мы отсортируем, поэтому я сохраню это для какой-то другой статьи. Другая проблема в том, что текст пересекается со своей собственной 'F'. Для этого действительно -нет конкретного решения. Если бы вы делали MMO и хотели, чтобы текст каждого -игрока всегда появлялся, вы могли бы попытаться заставить текст появляться над головой. Просто переместите -его +Y на какое-то количество единиц, достаточно, чтобы убедиться, что он всегда был выше игрока. +нет конкретного решения. Если бы вы делали MMO и хотели, чтобы текст каждого игрока всегда появлялся, +вы могли бы попытаться заставить текст появляться над головой. Просто переместите его +по +Y на некоторое количество единиц, достаточно, чтобы убедиться, что он всегда был выше игрока. -Вы также можете переместить его вперед к камере. Давайте сделаем это здесь просто так. -Поскольку 'pos' находится в пространстве вида, это означает, что он относительно глаза (который находится в 0,0,0 в пространстве вида). +Вы также можете переместить его вперед к камере. Давайте сделаем это здесь просто для удовольствия. +Поскольку 'pos' в пространстве вида, это означает, что он относительно глаза (который находится в 0,0,0 в пространстве вида). Поэтому если мы нормализуем его, мы получим единичный вектор, указывающий от глаза к этой точке, который мы можем затем -умножить на какое-то количество, чтобы переместить текст на определенное количество единиц к глазу или от него. +умножить на некоторое количество, чтобы переместить текст на определенное количество единиц к глазу или от него. - // потому что pos находится в пространстве вида, это означает, что это вектор от глаза к - // какой-то позиции. Поэтому перемещаем вдоль этого вектора назад к глазу на какое-то расстояние + // потому что pos в пространстве вида, это означает, что это вектор от глаза к + // некоторой позиции. Итак, перемещаемся вдоль этого вектора обратно к глазу на некоторое расстояние var fromEye = m4.normalize(pos); - var amountToMoveTowardEye = 150; // потому что F длиной 150 единиц + var amountToMoveTowardEye = 150; // потому что F 150 единиц длиной var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; @@ -299,16 +299,16 @@ viewProjectionMatrix, как в других примерах. Мы умножа -Проблема здесь в том, что Canvas 2D API производит только предварительно умноженные альфа значения. -Когда мы загружаем содержимое canvas в текстуру, WebGL пытается отменить предварительное умножение -значений, но он не может сделать это идеально, потому что предварительно умноженная альфа теряет информацию. +Проблема здесь в том, что Canvas 2D API производит только предумноженные альфа значения. +Когда мы загружаем содержимое canvas в текстуру, WebGL пытается отменить предумножение +значений, но он не может сделать это идеально, потому что предумноженная альфа имеет потери. -Чтобы исправить это, давайте скажем WebGL не отменять предварительное умножение +Чтобы исправить это, давайте скажем WebGL не отменять предумножение gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); -Это говорит WebGL поставлять предварительно умноженные альфа значения в `gl.texImage2D` и `gl.texSubImage2D`. -Если данные, переданные в `gl.texImage2D`, уже предварительно умножены, как это есть для данных Canvas 2D, то +Это говорит WebGL поставлять предумноженные альфа значения в `gl.texImage2D` и `gl.texSubImage2D`. +Если данные, переданные в `gl.texImage2D`, уже предумножены, как это для данных Canvas 2D, то WebGL может просто пропустить их. Нам также нужно изменить функцию смешивания @@ -316,23 +316,23 @@ WebGL может просто пропустить их. gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); Старая умножала исходный цвет на его альфа. Это то, что означает `SRC_ALPHA`. Но -теперь данные нашей текстуры уже были умножены на его альфа. Это то, что означает предварительно умноженная. +теперь данные нашей текстуры уже были умножены на их альфа. Это то, что означает предумноженная. Поэтому нам не нужно, чтобы GPU делал умножение. Установка на `ONE` означает умножить на 1. {{{example url="../webgl-text-texture-premultiplied-alpha.html" }}} Края исчезли сейчас. -Что, если вы хотите сохранить текст фиксированного размера, но все еще правильно сортировать? Ну, если вы помните -из [статьи о перспективе](webgl-3d-perspective.html), наша матрица перспективы собирается -масштабировать наш объект на `-Z`, чтобы заставить его становиться меньше на расстоянии. Поэтому мы можем просто масштабировать -на `-Z` умножить на какой-то желаемый-масштаб, чтобы компенсировать. +Что если вы хотите сохранить текст фиксированного размера, но все еще правильно сортировать? Ну, если вы помните +из [статьи о перспективе](webgl-3d-perspective.html), наша матрица перспективы будет +масштабировать наш объект на `-Z`, чтобы он становился меньше вдалеке. Итак, мы можем просто масштабировать +на `-Z` умножить на некоторый желаемый масштаб для компенсации. ... - // потому что pos находится в пространстве вида, это означает, что это вектор от глаза к - // какой-то позиции. Поэтому перемещаем вдоль этого вектора назад к глазу на какое-то расстояние + // потому что pos в пространстве вида, это означает, что это вектор от глаза к + // некоторой позиции. Итак, перемещаемся вдоль этого вектора обратно к глазу на некоторое расстояние var fromEye = normalize(pos); - var amountToMoveTowardEye = 150; // потому что F длиной 150 единиц + var amountToMoveTowardEye = 150; // потому что F 150 единиц длиной var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; @@ -347,27 +347,27 @@ WebGL может просто пропустить их. {{{example url="../webgl-text-texture-consistent-scale.html" }}} -Если вы хотите рисовать разный текст у каждого F, вы должны создать новую текстуру для каждого -F и просто обновить текстовые uniforms для этого F. +Если вы хотите рисовать разный текст у каждой F, вы должны создать новую текстуру для каждой +F и просто обновлять текстовые uniforms для этой F. - // создаем текстовые текстуры, одну для каждого F + // создаем текстовые текстуры, одну для каждой F var textTextures = [ - "анна", // 0 - "коллин", // 1 - "джеймс", // 2 - "дэнни", // 3 - "калин", // 4 - "хиро", // 5 - "эдди", // 6 - "шу", // 7 - "брайан", // 8 - "тами", // 9 - "рик", // 10 - "джин", // 11 - "натали",// 12, - "эван", // 13, - "сакура", // 14, - "кай", // 15, + "anna", // 0 + "colin", // 1 + "james", // 2 + "danny", // 3 + "kalin", // 4 + "hiro", // 5 + "eddie", // 6 + "shu", // 7 + "brian", // 8 + "tami", // 9 + "rick", // 10 + "gene", // 11 + "natalie",// 12, + "evan", // 13, + "sakura", // 14, + "kai", // 15, ].map(function(name) { var textCanvas = makeTextCanvas(name, 100, 26); var textWidth = textCanvas.width; @@ -394,7 +394,7 @@ F и просто обновить текстовые uniforms для этого // выбираем текстуру var tex = textTextures[ndx]; -Используем размер этой текстуры в наших матричных вычислениях +Используем размер этой текстуры в наших вычислениях матрицы var textMatrix = m4.translate(projectionMatrix, pos[0], pos[1], pos[2]); @@ -411,7 +411,7 @@ F и просто обновить текстовые uniforms для этого Было бы более полезно, если бы мы рендерили текст белым. Тогда мы могли бы умножить текст на цвет и сделать его любым цветом, который мы хотим. -Сначала мы изменим текстовый шейдер, чтобы умножать на цвет +Сначала мы изменим текстовый шейдер, чтобы умножить на цвет ... in vec2 v_texcoord; @@ -430,9 +430,9 @@ F и просто обновить текстовые uniforms для этого textCtx.fillStyle = "white"; -Затем мы сделаем некоторые цвета +Затем мы создадим некоторые цвета - // цвета, 1 для каждого F + // цвета, 1 для каждой F var colors = [ [0.0, 0.0, 0.0, 1], // 0 [1.0, 0.0, 0.0, 1], // 1 @@ -454,19 +454,19 @@ F и просто обновить текстовые uniforms для этого Во время рисования мы выбираем цвет - // устанавливаем цвет uniform + // устанавливаем uniform цвета textUniforms.u_color = colors[ndx]; Цвета {{{example url="../webgl-text-texture-different-colors.html" }}} -Эта техника на самом деле является техникой, которую большинство браузеров используют, когда они ускорены GPU. +Эта техника на самом деле является техникой, которую используют большинство браузеров, когда они ускорены GPU. Они генерируют текстуры с вашим HTML содержимым и всеми различными стилями, которые вы применили, и пока это содержимое не изменяется, они могут просто рендерить текстуру -снова, когда вы прокручиваете и т.д. Конечно, если вы обновляете вещи все время, то -эта техника может стать немного медленной, потому что перегенерация текстур и их повторная загрузка -в GPU - это относительно медленная операция. +снова при прокрутке и т.д. Конечно, если вы обновляете вещи все время, то +эта техника может стать немного медленной, потому что перегенерация текстур и повторная загрузка +их в GPU - это относительно медленная операция. В [следующей статье мы рассмотрим технику, которая, вероятно, лучше для случаев, когда вещи обновляются часто](webgl-text-glyphs.html). @@ -475,7 +475,7 @@ F и просто обновить текстовые uniforms для этого

Масштабирование текста без пикселизации

Вы можете заметить в примерах до того, как мы начали использовать постоянный размер, -текст становится очень пикселизированным, когда он приближается к камере. Как мы это исправляем? +текст становится очень пикселизованным, когда он приближается к камере. Как мы это исправляем?

Ну, честно говоря, не очень распространено масштабировать 2D текст в 3D. Посмотрите на большинство игр @@ -487,13 +487,13 @@ F и просто обновить текстовые uniforms для этого

Если вы действительно хотите масштабировать 2D текст в 3D, я не знаю никаких легких вариантов. Несколько сходу:

    -
  • Создайте разные размеры текстур с шрифтами при разных разрешениях. Вы затем +
  • Создайте разные размеры текстур с шрифтами в разных разрешениях. Вы затем используете текстуры более высокого разрешения, когда текст становится больше. Это называется -LODing (использование разных Уровней Детализации).
  • +LODing (использование разных уровней детализации).
  • Другой был бы рендеринг текстур с точным правильным размером текста каждый кадр. Это, вероятно, было бы действительно медленно.
  • Еще один был бы сделать 2D текст из геометрии. Другими словами, вместо -рисования текста в текстуру, сделать текст из множества и множества треугольников. Это +рисования текста в текстуру сделать текст из множества и множества треугольников. Это работает, но у этого есть другие проблемы в том, что маленький текст не будет рендериться хорошо, а большой текст вы начнете видеть треугольники.
  • Еще один - это использовать очень специальные шейдеры, которые рендерят кривые. Это очень круто, diff --git a/webgl/lessons/ru/webgl-tips.md b/webgl/lessons/ru/webgl-tips.md index 2af385642..d55261d18 100644 --- a/webgl/lessons/ru/webgl-tips.md +++ b/webgl/lessons/ru/webgl-tips.md @@ -122,12 +122,12 @@ const saveBlob = (function() { + var deltaTime = now - then; + // Запоминаем время + then = now; -+ + + // Каждый кадр увеличиваем вращение + rotation[1] += rotationSpeed * deltaTime; -+ + + drawScene(); -+ + + // Следующий кадр + requestAnimationFrame(renderLoop); + } @@ -253,11 +253,11 @@ document.querySelectorAll('canvas').forEach((canvas) => { draw(canvas.id); canvas.addEventListener('focus', () => { - draw('есть фокус, нажмите клавишу'); + draw('has focus press a key'); }); canvas.addEventListener('blur', () => { - draw('фокус потерян'); + draw('lost focus'); }); canvas.addEventListener('keydown', (e) => { @@ -266,22 +266,21 @@ document.querySelectorAll('canvas').forEach((canvas) => { }); ``` -Обратите внимание: первый канвас не принимает ввод с клавиатуры. -Второй — принимает, но с обводкой. Третий — и принимает, и без обводки. +Обратите внимание: первый канвас не может принимать ввод с клавиатуры. Второй может, но получает обводку. У третьего применены оба решения. {{{example url="../webgl-tips-tabindex.html"}}} --- - + -# WebGL-анимация как фон страницы +# Создание WebGL-анимации как фона -Частый вопрос — как сделать WebGL-анимацию фоном страницы? +Часто спрашивают, как сделать WebGL-анимацию фоном веб-страницы. Есть два очевидных способа: -* Задать канвасу CSS `position: fixed`, например: +* Установить CSS `position` канваса как `fixed`: ```css #canvas { @@ -293,24 +292,24 @@ document.querySelectorAll('canvas').forEach((canvas) => { } ``` -и `z-index: -1`. +и установить `z-index` в -1. -Минус: ваш JS должен интегрироваться со страницей, и если страница сложная, нужно следить, чтобы код WebGL не конфликтовал с остальным JS. +Небольшой недостаток этого решения: ваш JavaScript должен интегрироваться со страницей, и если у вас сложная страница, нужно убедиться, что JavaScript в WebGL-коде не конфликтует с JavaScript, который делает другие вещи на странице. * Использовать `iframe` -Так сделано [на главной странице этого сайта](/). +Это решение используется на [главной странице этого сайта](/). -Вставьте в страницу iframe, например: +В вашей веб-странице просто вставьте iframe, например: ```html
    - Ваш контент. + Ваш контент здесь.
    ``` -Затем стилизуйте iframe, чтобы он занимал всё окно и был на заднем плане (почти как выше для канваса), плюс уберите рамку: +Затем стилизуйте iframe, чтобы он заполнял окно и был на фоне — в основном тот же код, что мы использовали выше для канваса, но также нужно установить `border` в `none`, так как iframe по умолчанию имеют границу: ```css #background { diff --git a/webgl/lessons/ru/webgl-visualizing-the-camera.md b/webgl/lessons/ru/webgl-visualizing-the-camera.md index 684b0d129..0ab6dde8d 100644 --- a/webgl/lessons/ru/webgl-visualizing-the-camera.md +++ b/webgl/lessons/ru/webgl-visualizing-the-camera.md @@ -272,7 +272,7 @@ function render() { // рисуем объект для представления первой камеры { - // Создаём view matrix из матрицы второй камеры. + // Создаём матрицу вида из матрицы второй камеры. const viewMatrix = m4.inverse(cameraMatrix2); let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix); @@ -300,16 +300,16 @@ function render() { render(); ``` -И теперь мы можем видеть камеру, используемую для рендера левой сцены, в сцене справа. +И теперь мы можем видеть камеру, используемую для рендеринга левой сцены, в сцене справа. {{{example url="../webgl-visualize-camera.html"}}} Давайте также нарисуем что-то для представления frustum камеры. -Поскольку frustum представляет преобразование в clip space, мы можем сделать куб, представляющий clip space, +Поскольку frustum представляет преобразование в clip space, мы можем создать куб, который представляет clip space, и использовать обратную матрицу проекции для размещения его в сцене. -Сначала нужен куб линий clip space: +Сначала нам нужен куб линий clip space. ```js function createClipspaceCubeBufferInfo(gl) { @@ -340,7 +340,7 @@ function createClipspaceCubeBufferInfo(gl) { } ``` -Затем можем создать один и нарисовать его: +Затем мы можем создать один и нарисовать его: ```js const cameraScale = 20; @@ -351,10 +351,14 @@ const cameraVAO = twgl.createVAOFromBufferInfo( const clipspaceCubeBufferInfo = createClipspaceCubeBufferInfo(gl); const clipspaceCubeVAO = twgl.createVAOFromBufferInfo( gl, solidColorProgramInfo, clipspaceCubeBufferInfo); +``` + +И в коде рендеринга: +```js // рисуем объект для представления первой камеры { - // Создаём view matrix из матрицы камеры. + // Создаём матрицу вида из матрицы камеры. const viewMatrix = m4.inverse(cameraMatrix2); let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix); @@ -394,10 +398,9 @@ const clipspaceCubeVAO = twgl.createVAOFromBufferInfo( // вызывает gl.drawArrays или gl.drawElements twgl.drawBufferInfo(gl, clipspaceCubeBufferInfo, gl.LINES); } -} ``` -Давайте также сделаем так, чтобы можно было настраивать near и far параметры первой камеры: +Давайте также сделаем так, чтобы мы могли настраивать near и far параметры первой камеры: ```js const settings = { @@ -410,26 +413,24 @@ const settings = { cam1Far: 500, }; -... - - // Вычисляем матрицу перспективной проекции - const perspectiveProjectionMatrix = - m4.perspective(degToRad(settings.cam1FieldOfView), - aspect, - settings.cam1Near, - settings.cam1Far); +// Вычисляем матрицу перспективной проекции +const perspectiveProjectionMatrix = + m4.perspective(degToRad(settings.cam1FieldOfView), + aspect, + settings.cam1Near, + settings.cam1Far); ``` -и теперь мы можем видеть frustum тоже: +И теперь мы можем видеть frustum тоже: {{{example url="../webgl-visualize-camera-with-frustum.html"}}} Если вы настроите near или far плоскости или поле зрения так, чтобы они обрезали F, вы увидите, что представление frustum совпадает. -Будем ли мы использовать перспективную или ортографическую проекцию для камеры слева — это будет работать в любом случае, -потому что матрица проекции всегда преобразует в clip space, поэтому её обратная всегда возьмёт наш куб от +1 до -1 -и исказит его соответствующим образом. +Будем ли мы использовать перспективную проекцию или ортографическую проекцию для камеры слева, +это будет работать в любом случае, потому что матрица проекции всегда преобразует в clip space, +поэтому её обратная всегда будет брать наш куб от +1 до -1 и искажать его соответствующим образом. ```js const settings = { @@ -444,8 +445,6 @@ const settings = { cam1OrthoUnits: 120, }; -... - // Вычисляем матрицу проекции const perspectiveProjectionMatrix = settings.cam1Ortho ? m4.orthographic( @@ -461,4 +460,9 @@ const perspectiveProjectionMatrix = settings.cam1Ortho settings.cam1Far); ``` -{{{example url="../webgl-visualize-camera-with-orthographic.html"}}} \ No newline at end of file +{{{example url="../webgl-visualize-camera-with-orthographic.html"}}} + +Такой тип визуализации должен быть знаком любому, кто использовал 3D пакет моделирования, как [Blender](https://blender.org), +или 3D игровой движок с инструментами редактирования сцены, как [Unity](https://unity.com) или [Godot](https://godotengine.org/). + +Это также может быть довольно полезно для отладки. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md b/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md index f5ca949f4..56b7ae77e 100644 --- a/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md +++ b/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md @@ -77,4 +77,8 @@ Vertex Array Objects — это опциональная функция в WebGL webglUtils.createProgramFromSources(...); Я надеюсь, это делает более ясным, что это за функции - и где их найти. \ No newline at end of file + и где их найти. + + + + \ No newline at end of file diff --git a/webgl/lessons/ru/webgl1-to-webgl2.md b/webgl/lessons/ru/webgl1-to-webgl2.md index eced4e05a..39359ff37 100644 --- a/webgl/lessons/ru/webgl1-to-webgl2.md +++ b/webgl/lessons/ru/webgl1-to-webgl2.md @@ -197,4 +197,262 @@ WebGL2 **почти** на 100% обратно совместим с WebGL1. Обратите внимание, что это также верно для прикреплений framebuffer `HALF_FLOAT`. -> Если вам любопытно, это была *ошибка* в спецификации WebGL1. Что произошло, так это то, что WebGL1 \ No newline at end of file +> Если вам любопытно, это была *ошибка* в спецификации WebGL1. Что произошло, так это то, что WebGL1 +> был выпущен и `OES_texture_float` был добавлен, и просто предполагалось, что правильный +> способ использования его для рендеринга заключался в создании текстуры, прикреплении её к framebuffer +> и проверке её статуса. Позже кто-то указал, что согласно спецификации этого было +> недостаточно, потому что спецификация говорит, что цвета, записанные во фрагментном шейдере, всегда +> ограничиваются от 0 до 1. `EXT_color_buffer_float` убирает это ограничение зажима, +> но поскольку WebGL уже был выпущен около года, это сломало бы многие веб-сайты, если бы +> ограничение было применено. Для WebGL2 они смогли исправить это, и теперь вы должны включить +> `EXT_color_buffer_float`, чтобы использовать текстуры с плавающей точкой как прикрепления framebuffer. +> +> ПРИМЕЧАНИЕ: Насколько мне известно, по состоянию на март 2017 года очень немногие мобильные устройства +> поддерживают рендеринг в текстуры с плавающей точкой. + +## Объекты вершинных массивов + +Из всех функций выше та функция, которую я лично считаю, что вы должны +всегда ВСЕГДА использовать - это объекты вершинных массивов. Все остальное действительно +зависит от того, что вы пытаетесь сделать, но объекты вершинных массивов в частности +кажутся базовой функцией, которую всегда следует использовать. + +В WebGL1 без объектов вершинных массивов все данные об атрибутах +были глобальным состоянием WebGL. Вы можете представить это так + + var glState = { + attributeState: { + ELEMENT_ARRAY_BUFFER: null, + attributes: [ + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + { enable: ?, size: ?, type: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, }, + ], + }, + } + +Вызов функций типа `gl.vertexAttribPointer`, `gl.enableVertexAttribArray` и +`gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ??)` влиял на это глобальное состояние. +Перед каждой вещью, которую вы хотели нарисовать, вам нужно было настроить все атрибуты, и если вы +рисовали индексированные данные, вам нужно было установить `ELEMENT_ARRAY_BUFFER`. + +С объектами вершинных массивов все это `attributeState` выше становится *Вершинным массивом*. + +Другими словами + + var someVAO = gl.createVertexArray(); + +Создает новый экземпляр вещи выше, называемой `attributeState`. + + gl.bindVertexArray(someVAO); + +Это эквивалентно + + glState.attributeState = someVAO; + +Что это означает, так это то, что вы должны настроить все ваши атрибуты во время инициализации сейчас. + + // во время инициализации + for each model / geometry / ... + var vao = gl.createVertexArray() + gl.bindVertexArray(vao); + for each attribute + gl.enableVertexAttribArray(...); + gl.bindBuffer(gl.ARRAY_BUFFER, bufferForAttribute); + gl.vertexAttribPointer(...); + if indexed geometry + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); + gl.bindVertexArray(null); + +Затем во время рендеринга для использования конкретной геометрии все, что вам нужно сделать, +это + + gl.bindVertexArray(vaoForGeometry); + +В WebGL1 цикл инициализации выше появился бы во время рендеринга. +Это ОГРОМНОЕ ускорение! + +Есть несколько предостережений: + +1. расположения атрибутов зависят от программы. + + Если вы собираетесь использовать одну и ту же геометрию с несколькими + программами, рассмотрите ручное назначение расположений атрибутов. + В GLSL 300 es вы можете сделать это в шейдере. + + Например: + + layout(location = 0) in vec4 a_position; + layout(location = 1) in vec2 a_texcoord; + layout(location = 2) in vec3 a_normal; + layout(location = 3) in vec4 a_color; + + Устанавливает расположения 4 атрибутов. + + Вы также можете все еще делать это способом WebGL1, вызывая + `gl.bindAttribLocation` перед вызовом `gl.linkProgram`. + + Например: + + gl.bindAttribLocation(someProgram, 0, "a_position"); + gl.bindAttribLocation(someProgram, 1, "a_texcoord"); + gl.bindAttribLocation(someProgram, 2, "a_normal"); + gl.bindAttribLocation(someProgram, 3, "a_color"); + + Это означает, что вы можете заставить их быть совместимыми между несколькими шейдерными + программами. Если одной программе не нужны все атрибуты, + атрибуты, которые им нужны, все еще будут назначены на + те же расположения. + + Если вы не сделаете этого, вам понадобятся разные VAO для + разных шейдерных программ при использовании одной и той же геометрии ИЛИ + вам нужно будет просто делать вещь WebGL1 и не использовать + VAO и всегда настраивать атрибуты во время рендеринга, что медленно. + + ПРИМЕЧАНИЕ: из 2 методов выше я склоняюсь к использованию + `gl.bindAttribLocation`, потому что легко иметь это в одном + месте в моем коде, тогда как метод использования `layout(location = ?)` должен + быть во всех шейдерах, поэтому в интересах D.R.Y., `gl.bindAttribLocation` + кажется лучше. Может быть, если бы я использовал генератор шейдеров, то разницы бы не было. + +2. Всегда отвязывайте VAO, когда закончили + + gl.bindVertexArray(null); + + Это просто из моего собственного опыта. Если вы посмотрите выше, + состояние `ELEMENT_ARRAY_BUFFER` является частью вершинного массива. + + Итак, я столкнулся с этой проблемой. Я создал некоторую геометрию, затем + я создал VAO для этой геометрии и настроил атрибуты + и `ELEMENT_ARRAY_BUFFER`. Затем я создал еще немного + геометрии. Когда эта геометрия настраивала свои индексы, потому что + у меня все еще был привязан предыдущий VAO, индексы + повлияли на привязку `ELEMENT_ARRAY_BUFFER` для предыдущего + VAO. Мне потребовалось несколько часов для отладки. + + Итак, мое предложение - никогда не оставлять VAO привязанным, если вы закончили + с ним. Либо немедленно привяжите следующий VAO, который собираетесь + использовать, либо привяжите `null`, если закончили. + +Как упоминалось в начале, многие расширения из WebGL1 являются стандартными функциями +WebGL2, поэтому если вы использовали расширения в WebGL1, вам нужно будет +изменить ваш код, чтобы не использовать их как расширения в WebGL2. См. ниже. + +Два, которые требуют особого внимания: + +1. `OES_texture_float` и текстуры с плавающей точкой. + + Текстуры с плавающей точкой являются стандартной функцией WebGL2, но: + + * Возможность фильтрации текстур с плавающей точкой все еще является расширением: `OES_texture_float_linear`. + + * Возможность рендеринга в текстуру с плавающей точкой является расширением: `EXT_color_buffer_float`. + + * Создание текстуры с плавающей точкой отличается. Вы должны использовать один из новых внутренних форматов WebGL2 + как `RGBA32F`, `R32F` и т.д. Это отличается от расширения WebGL1 `OES_texture_float`, + в котором внутренний формат выводился из `type`, переданного в `texImage2D`. + +2. `WEBGL_depth_texture` и текстуры глубины. + + Аналогично предыдущему различию, для создания текстуры глубины в WebGL2 вы должны использовать один из + внутренних форматов WebGL2: `DEPTH_COMPONENT16`, `DEPTH_COMPONENT24`, + `DEPTH_COMPONENT32F`, `DEPTH24_STENCIL8` или `DEPTH32F_STENCIL8`, тогда как расширение WebGL1 + `WEBGL_depth_texture` использовало `DEPTH_COMPONENT` и `DEPTH_STENCIL_COMPONENT`. + +Это мой личный короткий список вещей, о которых нужно знать при переходе +с WebGL1 на WebGL2. [Есть еще больше вещей, которые вы можете делать в WebGL2](webgl2-whats-new.html). + +
    +

    Заставляем расширения WebGL1 выглядеть как WebGL2

    +

    Функции, которые были в расширениях в WebGL1, теперь находятся в основном +контексте в WebGL2. Например, в WebGL1

    +
    +var ext = gl.getExtension("OES_vertex_array_object");
    +if (!ext) {
    +  // сказать пользователю, что у него нет требуемого расширения или обойти это
    +} else {
    +  var someVAO = ext.createVertexArrayOES();
    +}
    +
    +

    +против в webgl2 +

    +
    +var someVAO = gl.createVertexArray();
    +
    +

    Как вы можете видеть, если вы хотите, чтобы ваш код работал как в WebGL1, так и в WebGL2, то +это может представлять некоторые проблемы.

    +

    Одним из обходных путей было бы копирование расширений WebGL1 в контекст WebGL во время инициализации. +Таким образом, остальная часть вашего кода может остаться той же. Пример:

    +
    {{#escapehtml}}
    +const gl = someCanvas.getContext("webgl");
    +const haveVAOs = getAndApplyExtension(gl, "OES_vertex_array_object");
    +
    +function getAndApplyExtension(gl, name) {
    +  const ext = gl.getExtension(name);
    +  if (!ext) {
    +    return null;
    +  }
    +  const fnSuffix = name.split("_")[0];
    +  const enumSuffix = '_' + fnSuffix;
    +  for (const key in ext) {
    +    const value = ext[key];
    +    const isFunc = typeof (value) === 'function';
    +    const suffix = isFunc ? fnSuffix : enumSuffix;
    +    let name = key;
    +    // примеры, где это не так, это WEBGL_compressed_texture_s3tc
    +    // и WEBGL_compressed_texture_pvrtc
    +    if (key.endsWith(suffix)) {
    +      name = key.substring(0, key.length - suffix.length);
    +    }
    +    if (gl[name] !== undefined) {
    +      if (!isFunc && gl[name] !== value) {
    +        console.warn("conflict:", name, gl[name], value, key);
    +      }
    +    } else {
    +      if (isFunc) {
    +        gl[name] = function(origFn) {
    +          return function() {
    +            return origFn.apply(ext, arguments);
    +          };
    +        }(value);
    +      } else {
    +        gl[name] = value;
    +      }
    +    }
    +  }
    +  return ext;
    +}
    +{{/escapehtml}}
    +

    Теперь ваш код может в основном просто работать одинаково на обоих. Пример:

    +
    {{#escapehtml}}
    +if (haveVAOs) {
    +  var someVAO = gl.createVertexArray();
    +  ...
    +} else {
    +  ... делать что-то для отсутствия VAO.
    +}
    +{{/escapehtml}}
    +

    Альтернативой было бы делать что-то вроде этого

    +
    {{#escapehtml}}
    +if (haveVAOs) {
    +  if (isWebGL2)
    +     someVAO = gl.createVertexArray();
    +  } else {
    +     someVAO = vaoExt.createVertexArrayOES();
    +  }
    +  ...
    +} else {
    +  ... делать что-то для отсутствия VAO.
    +}
    +{{/escapehtml}}
    +

    Примечание: В случае объектов вершинных массивов в частности я предлагаю вам использовать полифилл +чтобы они были везде. VAO доступны на большинстве систем. На тех немногих системах +где они недоступны, полифилл обработает это за вас, и ваш код +может остаться простым.

    +
    \ No newline at end of file