diff --git a/webgl/lessons/ru/index.md b/webgl/lessons/ru/index.md
new file mode 100644
index 000000000..1673db28b
--- /dev/null
+++ b/webgl/lessons/ru/index.md
@@ -0,0 +1,29 @@
+Title: WebGL2 Основы
+
+Изучайте WebGL с основ теории!
+
+Это серия уроков, которые начинаются с базовой теории WebGL2.
+Они не похожи на большинство других туториалов, которые адаптированы из старых статей OpenGL.
+Они современны, отбрасывают устаревшие концепции и дают вам полное понимание того, как работает WebGL.
+
+Эти статьи специально посвящены WebGL2.
+Если вас интересует WebGL 1.0, перейдите [сюда](https://webglfundamentals.org)
+Если вы уже знакомы с WebGL1, возможно, вам будет интересно посмотреть эти статьи
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2-textures.md b/webgl/lessons/ru/webgl-2-textures.md
new file mode 100644
index 000000000..536f4597a
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2-textures.md
@@ -0,0 +1,257 @@
+Title: WebGL2 Использование 2 или более текстур
+Description: Как использовать 2 или более текстур в WebGL
+TOC: Использование 2 или более текстур
+
+
+Эта статья является продолжением [Обработки изображений в WebGL](webgl-image-processing.html).
+Если вы не читали её, я рекомендую [начать оттуда](webgl-image-processing.html).
+
+Теперь самое время ответить на вопрос: "Как использовать 2 или более текстур?"
+
+Это довольно просто. Давайте [вернемся на несколько уроков назад к нашему
+первому шейдеру, который рисует одно изображение](webgl-image-processing.html) и обновим его для 2 изображений.
+
+Первое, что нам нужно сделать - это изменить наш код, чтобы мы могли загрузить 2 изображения. Это не
+действительно WebGL вещь, это HTML5 JavaScript вещь, но мы можем с этим справиться.
+Изображения загружаются асинхронно, что может потребовать некоторого привыкания, если вы не
+начинали с веб-программирования.
+
+Есть в основном 2 способа, которыми мы могли бы это обработать. Мы могли бы попытаться структурировать наш код
+так, чтобы он работал без текстур, и по мере загрузки текстур программа обновлялась.
+Мы сохраним этот метод для более поздней статьи.
+
+В данном случае мы будем ждать загрузки всех изображений перед тем, как что-либо рисовать.
+
+Сначала давайте изменим код, который загружает изображение, в функцию. Это довольно просто.
+Он создает новый объект `Image`, устанавливает URL для загрузки и устанавливает обратный вызов,
+который будет вызван, когда изображение закончит загружаться.
+
+```js
+function loadImage (url, callback) {
+ var image = new Image();
+ image.src = url;
+ image.onload = callback;
+ return image;
+}
+```
+
+Теперь давайте создадим функцию, которая загружает массив URL и генерирует массив изображений.
+Сначала мы устанавливаем `imagesToLoad` равным количеству изображений, которые мы собираемся загрузить. Затем мы делаем
+обратный вызов, который мы передаем в `loadImage`, уменьшаем `imagesToLoad`. Когда `imagesToLoad` становится
+равным 0, все изображения загружены, и мы передаем массив изображений в обратный вызов.
+
+```js
+function loadImages(urls, callback) {
+ var images = [];
+ var imagesToLoad = urls.length;
+
+ // Вызывается каждый раз, когда изображение заканчивает загружаться.
+ var onImageLoad = function() {
+ --imagesToLoad;
+ // Если все изображения загружены, вызываем обратный вызов.
+ if (imagesToLoad === 0) {
+ callback(images);
+ }
+ };
+
+ for (var ii = 0; ii < imagesToLoad; ++ii) {
+ var image = loadImage(urls[ii], onImageLoad);
+ images.push(image);
+ }
+}
+```
+
+Теперь мы вызываем loadImages так:
+
+```js
+function main() {
+ loadImages([
+ "resources/leaves.jpg",
+ "resources/star.jpg",
+ ], render);
+}
+```
+
+Далее мы изменяем шейдер для использования 2 текстур. В данном случае мы будем умножать одну текстуру на другую.
+
+```
+#version 300 es
+precision highp float;
+
+// наши текстуры
+*uniform sampler2D u_image0;
+*uniform sampler2D u_image1;
+
+// координаты текстуры, переданные из вершинного шейдера.
+in vec2 v_texCoord;
+
+// нам нужно объявить выход для фрагментного шейдера
+out vec2 outColor;
+
+void main() {
+* vec4 color0 = texture2D(u_image0, v_texCoord);
+* vec4 color1 = texture2D(u_image1, v_texCoord);
+* outColor = color0 * color1;
+}
+```
+
+Нам нужно создать 2 WebGL объекта текстур.
+
+```js
+ // создаем 2 текстуры
+ var textures = [];
+ for (var ii = 0; ii < 2; ++ii) {
+ var texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+
+ // Устанавливаем параметры, чтобы нам не нужны были мипы
+ 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);
+
+ // Загружаем изображение в текстуру.
+ var mipLevel = 0; // самый большой мип
+ var internalFormat = gl.RGBA; // формат, который мы хотим в текстуре
+ var srcFormat = gl.RGBA; // формат данных, которые мы поставляем
+ var srcType = gl.UNSIGNED_BYTE; // тип данных, которые мы поставляем
+ gl.texImage2D(gl.TEXTURE_2D,
+ mipLevel,
+ internalFormat,
+ srcFormat,
+ srcType,
+ images[ii]);
+
+ // добавляем текстуру в массив текстур.
+ textures.push(texture);
+ }
+```
+
+WebGL имеет что-то, называемое "блоками текстур". Вы можете думать об этом как о массиве ссылок
+на текстуры. Вы говорите шейдеру, какой блок текстуры использовать для каждого сэмплера.
+
+```js
+ // ищем местоположения сэмплеров.
+ var u_image0Location = gl.getUniformLocation(program, "u_image0");
+ var u_image1Location = gl.getUniformLocation(program, "u_image1");
+
+ ...
+
+ // устанавливаем, какие блоки текстур использовать для рендеринга.
+ gl.uniform1i(u_image0Location, 0); // блок текстуры 0
+ gl.uniform1i(u_image1Location, 1); // блок текстуры 1
+```
+
+Затем мы должны привязать текстуру к каждому из этих блоков текстур.
+
+```js
+ // Устанавливаем каждый блок текстуры для использования определенной текстуры.
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, textures[0]);
+ gl.activeTexture(gl.TEXTURE1);
+ gl.bindTexture(gl.TEXTURE_2D, textures[1]);
+```
+
+2 изображения, которые мы загружаем, выглядят так:
+
+
+
+
+И вот результат, если мы умножим их вместе, используя WebGL.
+
+{{{example url="../webgl-2-textures.html" }}}
+
+Некоторые вещи, которые я должен разобрать.
+
+Простой способ думать о блоках текстур - это что-то вроде этого: Все функции текстур
+работают с "активным блоком текстуры". "Активный блок текстуры" - это просто глобальная переменная,
+которая является индексом блока текстуры, с которым вы хотите работать. Каждый блок текстуры в WebGL2 имеет 4 цели.
+Цель TEXTURE_2D, цель TEXTURE_3D, цель TEXTURE_2D_ARRAY и цель TEXTURE_CUBE_MAP.
+Каждая функция текстуры работает с указанной целью на текущем активном блоке текстуры.
+Если бы вы реализовали WebGL в JavaScript, это выглядело бы примерно так:
+
+```js
+var getContext = function() {
+ var textureUnits = [
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ { TEXTURE_2D: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, TEXTURE_CUBE_MAP: null, },
+ ];
+ var activeTextureUnit = 0;
+
+ var activeTexture = function(unit) {
+ // конвертируем enum блока в индекс.
+ var index = unit - gl.TEXTURE0;
+ // Устанавливаем активный блок текстуры
+ activeTextureUnit = index;
+ };
+
+ var bindTexture = function(target, texture) {
+ // Устанавливаем текстуру для цели активного блока текстуры.
+ textureUnits[activeTextureUnit][target] = texture;
+ };
+
+ var texImage2D = function(target, ...args) {
+ // Вызываем texImage2D на текущей текстуре активного блока текстуры
+ var texture = textureUnits[activeTextureUnit][target];
+ texture.image2D(...args);
+ };
+
+ var texImage3D = function(target, ...args) {
+ // Вызываем texImage3D на текущей текстуре активного блока текстуры
+ var texture = textureUnits[activeTextureUnit][target];
+ texture.image3D(...args);
+ };
+
+ // возвращаем WebGL API
+ return {
+ activeTexture: activeTexture,
+ bindTexture: bindTexture,
+ texImage2D: texImage2D,
+ texImage3D: texImage3D,
+ };
+};
+```
+
+Шейдеры принимают индексы в блоки текстур. Надеюсь, это делает эти 2 строки более ясными.
+
+```js
+ gl.uniform1i(u_image0Location, 0); // блок текстуры 0
+ gl.uniform1i(u_image1Location, 1); // блок текстуры 1
+```
+
+Одна вещь, о которой нужно знать: при установке uniform'ов вы используете индексы для блоков текстур,
+но при вызове gl.activeTexture вы должны передать специальные константы gl.TEXTURE0, gl.TEXTURE1 и т.д.
+К счастью, константы последовательные, поэтому вместо этого:
+
+```js
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, textures[0]);
+ gl.activeTexture(gl.TEXTURE1);
+ gl.bindTexture(gl.TEXTURE_2D, textures[1]);
+```
+
+Мы могли бы сделать это:
+
+```js
+ gl.activeTexture(gl.TEXTURE0 + 0);
+ gl.bindTexture(gl.TEXTURE_2D, textures[0]);
+ gl.activeTexture(gl.TEXTURE0 + 1);
+ gl.bindTexture(gl.TEXTURE_2D, textures[1]);
+```
+
+или это:
+
+```js
+ for (var ii = 0; ii < 2; ++ii) {
+ gl.activeTexture(gl.TEXTURE0 + ii);
+ gl.bindTexture(gl.TEXTURE_2D, textures[ii]);
+ }
+```
+
+Надеюсь, этот небольшой шаг помогает объяснить, как использовать несколько текстур в одном вызове отрисовки в WebGL.
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-drawimage.md b/webgl/lessons/ru/webgl-2d-drawimage.md
new file mode 100644
index 000000000..d4bdc3ce6
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-drawimage.md
@@ -0,0 +1,490 @@
+Title: WebGL2 Реализация DrawImage
+Description: Как реализовать функцию drawImage canvas 2d в WebGL
+TOC: 2D - DrawImage
+
+
+Эта статья является продолжением [WebGL ортографической 3D](webgl-3d-orthographic.html).
+Если вы не читали её, я рекомендую [начать оттуда](webgl-3d-orthographic.html).
+Вы также должны знать, как работают текстуры и координаты текстур, пожалуйста, прочитайте
+[WebGL 3D текстуры](webgl-3d-textures.html).
+
+Для реализации большинства игр в 2D требуется всего одна функция для рисования изображения. Конечно, некоторые 2D игры
+делают причудливые вещи с линиями и т.д., но если у вас есть только способ нарисовать 2D изображение на экране,
+вы можете сделать большинство 2D игр.
+
+Canvas 2D API имеет очень гибкую функцию для рисования изображений, называемую `drawImage`. У неё есть 3 версии:
+
+ ctx.drawImage(image, dstX, dstY);
+ ctx.drawImage(image, dstX, dstY, dstWidth, dstHeight);
+ ctx.drawImage(image, srcX, srcY, srcWidth, srcHeight,
+ dstX, dstY, dstWidth, dstHeight);
+
+Учитывая всё, что вы изучили до сих пор, как бы вы реализовали это в WebGL? Ваше первое
+решение может быть генерировать вершины, как это делали некоторые из первых статей на этом сайте.
+Отправка вершин в GPU обычно является медленной операцией (хотя есть случаи, когда это будет быстрее).
+
+Здесь вступает в игру вся суть WebGL. Всё дело в творческом написании
+шейдера и затем творческом использовании этого шейдера для решения вашей проблемы.
+
+Давайте начнем с первой версии:
+
+ ctx.drawImage(image, x, y);
+
+Она рисует изображение в позиции `x, y` того же размера, что и изображение.
+Чтобы сделать аналогичную WebGL функцию, мы могли бы загрузить вершины для `x, y`, `x + width, y`, `x, y + height`,
+и `x + width, y + height`, затем по мере рисования разных изображений в разных местах
+мы бы генерировали разные наборы вершин. На самом деле [это именно то, что мы делали в нашей первой
+статье](webgl-fundamentals.html).
+
+Гораздо более распространенный способ - это просто использовать единичный квадрат. Мы загружаем один квадрат размером 1 единица. Затем мы
+используем [матричную математику](webgl-2d-matrices.html) для масштабирования и перемещения этого единичного квадрата так, чтобы
+он оказался в нужном месте.
+
+Вот код.
+
+Сначала нам нужен простой вершинный шейдер:
+
+ #version 300 es
+
+ in vec4 a_position;
+ in vec2 a_texcoord;
+
+ uniform mat4 u_matrix;
+ uniform mat4 u_textureMatrix;
+
+ out vec2 v_texcoord;
+
+ void main() {
+ gl_Position = u_matrix * a_position;
+ v_texcoord = (u_textureMatrix * vec4(a_texcoord, 0, 1)).xy;
+ }
+
+И простой фрагментный шейдер:
+
+ #version 300 es
+ precision highp float;
+
+ in vec2 v_texcoord;
+
+ uniform sampler2D texture;
+
+ out vec4 outColor;
+
+ void main() {
+ outColor = texture(texture, v_texcoord);
+ }
+
+И теперь функция:
+
+ function drawImage(tex, texWidth, texHeight, dstX, dstY) {
+ gl.useProgram(program);
+
+ // Настраиваем атрибуты для квадрата
+ gl.bindVertexArray(vao);
+
+ var textureUnit = 0;
+ // шейдер, на который мы помещаем текстуру на блок текстуры 0
+ gl.uniform1i(textureLocation, textureUnit);
+
+ // Привязываем текстуру к блоку текстуры 0
+ gl.activeTexture(gl.TEXTURE0 + textureUnit);
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+
+ // эта матрица будет конвертировать из пикселей в пространство отсечения
+ var matrix = m4.orthographic(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
+
+ // перемещаем наш квадрат в dstX, dstY
+ matrix = m4.translate(matrix, dstX, dstY, 0);
+
+ // масштабируем наш единичный квадрат
+ // с 1 единицы до texWidth, texHeight единиц
+ matrix = m4.scale(matrix, texWidth, texHeight, 1);
+
+ // Устанавливаем матрицу.
+ gl.uniformMatrix4fv(matrixLocation, false, matrix);
+
+ // рисуем квадрат (2 треугольника, 6 вершин)
+ var offset = 0;
+ var count = 6;
+ gl.drawArrays(gl.TRIANGLES, offset, count);
+ }
+
+Давайте загрузим некоторые изображения в текстуры:
+
+ // создает информацию о текстуре { width: w, height: h, texture: tex }
+ // Текстура начнет с 1x1 пикселей и будет обновлена
+ // когда изображение загрузится
+ function loadImageAndCreateTextureInfo(url) {
+ var tex = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+
+ 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);
+
+ var textureInfo = {
+ width: 1, // мы не знаем размер, пока он не загрузится
+ height: 1,
+ texture: tex,
+ };
+ var img = new Image();
+ img.addEventListener('load', function() {
+ textureInfo.width = img.width;
+ textureInfo.height = img.height;
+
+ gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
+ gl.generateMipmap(gl.TEXTURE_2D);
+ });
+
+ return textureInfo;
+ }
+
+ var textureInfos = [
+ loadImageAndCreateTextureInfo('resources/star.jpg'),
+ loadImageAndCreateTextureInfo('resources/leaves.jpg'),
+ loadImageAndCreateTextureInfo('resources/keyboard.jpg'),
+ ];
+
+И давайте нарисуем их в случайных местах:
+
+ var drawInfos = [];
+ var numToDraw = 9;
+ var speed = 60;
+ for (var ii = 0; ii < numToDraw; ++ii) {
+ var drawInfo = {
+ x: Math.random() * gl.canvas.width,
+ y: Math.random() * gl.canvas.height,
+ dx: Math.random() > 0.5 ? -1 : 1,
+ dy: Math.random() > 0.5 ? -1 : 1,
+ textureInfo: textureInfos[Math.random() * textureInfos.length | 0],
+ };
+ drawInfos.push(drawInfo);
+ }
+
+ function update(deltaTime) {
+ drawInfos.forEach(function(drawInfo) {
+ drawInfo.x += drawInfo.dx * speed * deltaTime;
+ drawInfo.y += drawInfo.dy * speed * deltaTime;
+ if (drawInfo.x < 0) {
+ drawInfo.dx = 1;
+ }
+ if (drawInfo.x >= gl.canvas.width) {
+ drawInfo.dx = -1;
+ }
+ if (drawInfo.y < 0) {
+ drawInfo.dy = 1;
+ }
+ if (drawInfo.y >= gl.canvas.height) {
+ drawInfo.dy = -1;
+ }
+ });
+ }
+
+ function draw() {
+ webglUtils.resizeCanvasToDisplaySize(gl.canvas);
+
+ // Говорим WebGL, как конвертировать из пространства отсечения в пиксели
+ 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);
+
+ drawInfos.forEach(function(drawInfo) {
+ drawImage(
+ drawInfo.textureInfo.texture,
+ drawInfo.textureInfo.width,
+ drawInfo.textureInfo.height,
+ drawInfo.x,
+ drawInfo.y);
+ });
+ }
+
+ var then = 0;
+ function render(time) {
+ var now = time * 0.001;
+ var deltaTime = Math.min(0.1, now - then);
+ then = now;
+
+ update(deltaTime);
+ draw();
+
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+
+Вы можете увидеть это в действии здесь:
+
+{{{example url="../webgl-2d-drawimage-01.html" }}}
+
+Обработка версии 2 оригинальной canvas функции `drawImage`:
+
+ ctx.drawImage(image, dstX, dstY, dstWidth, dstHeight);
+
+Действительно ничем не отличается. Мы просто используем `dstWidth` и `dstHeight` вместо
+`texWidth` и `texHeight`.
+
+ *function drawImage(tex, texWidth, texHeight, dstX, dstY, dstWidth, dstHeight) {
+ + if (dstWidth === undefined) {
+ + dstWidth = texWidth;
+ + }
+ +
+ + if (dstHeight === undefined) {
+ + dstHeight = texHeight;
+ + }
+
+ gl.useProgram(program);
+
+ // Настраиваем атрибуты для квадрата
+ gl.bindVertexArray(vao);
+
+ var textureUnit = 0;
+ // шейдер, на который мы помещаем текстуру на блок текстуры 0
+ gl.uniform1i(textureLocation, textureUnit);
+
+ // Привязываем текстуру к блоку текстуры 0
+ gl.activeTexture(gl.TEXTURE0 + textureUnit);
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+
+ // эта матрица будет конвертировать из пикселей в пространство отсечения
+ var matrix = m4.orthographic(0, canvas.width, canvas.height, 0, -1, 1);
+
+ // перемещаем наш квадрат в dstX, dstY
+ matrix = m4.translate(matrix, dstX, dstY, 0);
+
+ // масштабируем наш единичный квадрат
+ * // с 1 единицы до dstWidth, dstHeight единиц
+ * matrix = m4.scale(matrix, dstWidth, dstHeight, 1);
+
+ // Устанавливаем матрицу.
+ gl.uniformMatrix4fv(matrixLocation, false, matrix);
+
+ // рисуем квадрат (2 треугольника, 6 вершин)
+ var offset = 0;
+ var count = 6;
+ gl.drawArrays(gl.TRIANGLES, offset, count);
+ }
+
+Я обновил код для использования разных размеров:
+
+{{{example url="../webgl-2d-drawimage-02.html" }}}
+
+Так что это было легко. Но что насчет 3-й версии canvas `drawImage`?
+
+ ctx.drawImage(image, srcX, srcY, srcWidth, srcHeight,
+ dstX, dstY, dstWidth, dstHeight);
+
+Для выбора части текстуры нам нужно манипулировать координатами текстуры. Как
+работают координаты текстуры, было [покрыто в статье о текстурах](webgl-3d-textures.html).
+В той статье мы вручную создавали координаты текстуры, что является очень распространенным способом сделать это,
+но мы также можем создавать их на лету, и точно так же, как мы манипулируем нашими позициями, используя
+матрицу, мы можем аналогично манипулировать координатами текстуры, используя другую матрицу.
+
+Давайте добавим матрицу текстуры в вершинный шейдер и умножим координаты текстуры
+на эту матрицу текстуры.
+
+ #version 300 es
+
+ in vec4 a_position;
+ in vec2 a_texcoord;
+
+ uniform mat4 u_matrix;
+ uniform mat4 u_textureMatrix;
+
+ out vec2 v_texcoord;
+
+ void main() {
+ gl_Position = u_matrix * a_position;
+ v_texcoord = (u_textureMatrix * vec4(a_texcoord, 0, 1)).xy;
+ }
+
+Теперь нам нужно найти местоположение матрицы текстуры:
+
+ var matrixLocation = gl.getUniformLocation(program, "u_matrix");
+ var textureMatrixLocation = gl.getUniformLocation(program, "u_textureMatrix");
+
+И внутри `drawImage` нам нужно установить её так, чтобы она выбирала часть текстуры, которую мы хотим.
+Мы знаем, что координаты текстуры также эффективно являются единичным квадратом, поэтому это очень похоже на
+то, что мы уже сделали для позиций.
+
+ *function drawImage(
+ * tex, texWidth, texHeight,
+ * srcX, srcY, srcWidth, srcHeight,
+ * dstX, dstY, dstWidth, dstHeight) {
+ + if (dstX === undefined) {
+ + dstX = srcX;
+ + srcX = 0;
+ + }
+ + if (dstY === undefined) {
+ + dstY = srcY;
+ + srcY = 0;
+ + }
+ + if (srcWidth === undefined) {
+ + srcWidth = texWidth;
+ + }
+ + if (srcHeight === undefined) {
+ + srcHeight = texHeight;
+ + }
+ if (dstWidth === undefined) {
+ * dstWidth = srcWidth;
+ + srcWidth = texWidth;
+ }
+ if (dstHeight === undefined) {
+ * dstHeight = srcHeight;
+ + srcHeight = texHeight;
+ }
+
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+
+ // эта матрица будет конвертировать из пикселей в пространство отсечения
+ var matrix = m4.orthographic(
+ 0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
+
+ // перемещаем наш квадрат в dstX, dstY
+ matrix = m4.translate(matrix, dstX, dstY, 0);
+
+ // масштабируем наш единичный квадрат
+ // с 1 единицы до dstWidth, dstHeight единиц
+ matrix = m4.scale(matrix, dstWidth, dstHeight, 1);
+
+ // Устанавливаем матрицу.
+ gl.uniformMatrix4fv(matrixLocation, false, matrix);
+
+ + // Поскольку координаты текстуры идут от 0 до 1
+ + // и поскольку наши координаты текстуры уже являются единичным квадратом
+ + // мы можем выбрать область текстуры, масштабируя единичный квадрат
+ + // вниз
+ + var texMatrix = m4.translation(srcX / texWidth, srcY / texHeight, 0);
+ + texMatrix = m4.scale(texMatrix, srcWidth / texWidth, srcHeight / texHeight, 1);
+ +
+ + // Устанавливаем матрицу текстуры.
+ + gl.uniformMatrix4fv(textureMatrixLocation, false, texMatrix);
+
+ // рисуем квадрат (2 треугольника, 6 вершин)
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ }
+
+Я также обновил код для выбора частей текстур. Вот результат:
+
+{{{example url="../webgl-2d-drawimage-03.html" }}}
+
+В отличие от canvas 2D API, наша WebGL версия обрабатывает случаи, которые canvas 2D `drawImage` не обрабатывает.
+
+Во-первых, мы можем передать отрицательную ширину или высоту для источника или назначения. Отрицательная `srcWidth`
+будет выбирать пиксели слева от `srcX`. Отрицательная `dstWidth` будет рисовать слева от `dstX`.
+В canvas 2D API это ошибки в лучшем случае или неопределенное поведение в худшем.
+
+{{{example url="../webgl-2d-drawimage-04.html" }}}
+
+Другое - поскольку мы используем матрицу, мы можем делать [любую матричную математику, которую хотим](webgl-2d-matrices.html).
+
+Например, мы могли бы повернуть координаты текстуры вокруг центра текстуры.
+
+Изменяя код матрицы текстуры на это:
+
+ * // точно как 2d матрица проекции, кроме как в пространстве текстуры (0 до 1)
+ * // вместо пространства отсечения. Эта матрица помещает нас в пространство пикселей.
+ * var texMatrix = m4.scaling(1 / texWidth, 1 / texHeight, 1);
+ *
+ * // Нам нужно выбрать место для поворота вокруг
+ * // Мы переместимся в середину, повернем, затем вернемся обратно
+ * var texMatrix = m4.translate(texMatrix, texWidth * 0.5, texHeight * 0.5, 0);
+ * var texMatrix = m4.zRotate(texMatrix, srcRotation);
+ * var texMatrix = m4.translate(texMatrix, texWidth * -0.5, texHeight * -0.5, 0);
+ *
+ * // потому что мы в пространстве пикселей
+ * // масштаб и перемещение теперь в пикселях
+ * var texMatrix = m4.translate(texMatrix, srcX, srcY, 0);
+ * var texMatrix = m4.scale(texMatrix, srcWidth, srcHeight, 1);
+
+ // Устанавливаем матрицу текстуры.
+ gl.uniformMatrix4fv(textureMatrixLocation, false, texMatrix);
+
+И вот это:
+
+{{{example url="../webgl-2d-drawimage-05.html" }}}
+
+вы можете увидеть одну проблему, которая заключается в том, что из-за поворота иногда мы видим за
+краем текстуры. Поскольку она установлена на `CLAMP_TO_EDGE`, край просто повторяется.
+
+Мы могли бы исправить это, отбрасывая любые пиксели вне диапазона от 0 до 1 внутри шейдера.
+`discard` немедленно выходит из шейдера без записи пикселя.
+
+ #version 300 es
+ precision highp float;
+
+ in vec2 v_texcoord;
+
+ uniform sampler2D texture;
+
+ out vec4 outColor;
+
+ void main() {
+ + if (v_texcoord.x < 0.0 ||
+ + v_texcoord.y < 0.0 ||
+ + v_texcoord.x > 1.0 ||
+ + v_texcoord.y > 1.0) {
+ + discard;
+ + }
+ outColor = texture(texture, v_texcoord);
+ }
+
+И теперь углы исчезли:
+
+{{{example url="../webgl-2d-drawimage-06.html" }}}
+
+или, может быть, вы хотели бы использовать сплошной цвет, когда координаты текстуры находятся вне текстуры:
+
+ #version 300 es
+ precision highp float;
+
+ in vec2 v_texcoord;
+
+ uniform sampler2D texture;
+
+ out vec4 outColor;
+
+ void main() {
+ if (v_texcoord.x < 0.0 ||
+ v_texcoord.y < 0.0 ||
+ v_texcoord.x > 1.0 ||
+ v_texcoord.y > 1.0) {
+ * outColor = vec4(0, 0, 1, 1); // синий
+ + return;
+ }
+ outColor = texture(texture, v_texcoord);
+ }
+
+{{{example url="../webgl-2d-drawimage-07.html" }}}
+
+Небо действительно является пределом. Всё зависит от вашего творческого использования шейдеров.
+
+Далее [мы реализуем стек матриц canvas 2d](webgl-2d-matrix-stack.html).
+
+
+
Небольшая оптимизация
+
Я не рекомендую эту оптимизацию. Скорее я хочу указать
+на более творческое мышление, поскольку WebGL - это всё о творческом использовании функций,
+которые он предоставляет.
+
Вы могли заметить, что мы используем единичный квадрат для наших позиций, и эти позиции
+единичного квадрата точно соответствуют нашим координатам текстуры. Как таковые, мы можем использовать позиции
+как координаты текстуры.
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-matrices.md b/webgl/lessons/ru/webgl-2d-matrices.md
new file mode 100644
index 000000000..d66886f0b
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-matrices.md
@@ -0,0 +1,201 @@
+Title: WebGL2 2D Матрицы
+Description: Как работает матричная математика, объяснено простыми и понятными инструкциями.
+TOC: 2D Матрицы
+
+
+Этот пост является продолжением серии постов о WebGL. Первый
+[начался с основ](webgl-fundamentals.html), а предыдущий
+был [о масштабировании 2D геометрии](webgl-2d-scale.html).
+
+
+Если у вас мало или нет опыта с матрицами, то смело
+пропустите ссылку выше пока и продолжайте чтение.
+
+
+
+В последних 3 постах мы прошли, как [перемещать геометрию](webgl-2d-translation.html),
+[поворачивать геометрию](webgl-2d-rotation.html) и [масштабировать геометрию](webgl-2d-scale.html).
+Перемещение, поворот и масштабирование каждый считается типом 'преобразования'.
+Каждое из этих преобразований требовало изменений в шейдере, и каждое
+из 3 преобразований зависело от порядка.
+В [нашем предыдущем примере](webgl-2d-scale.html) мы масштабировали, затем поворачивали,
+затем перемещали. Если бы мы применили их в другом порядке, мы получили бы
+другой результат.
+
+Например, вот масштабирование 2, 1, поворот на 30 градусов
+и перемещение на 100, 0.
+
+
+
+А вот перемещение на 100,0, поворот на 30 градусов и масштабирование 2, 1
+
+
+
+Результаты совершенно разные. Еще хуже, если бы нам нужен был
+второй пример, нам пришлось бы написать другой шейдер, который применял
+перемещение, поворот и масштабирование в нашем новом желаемом порядке.
+
+Ну, некоторые люди намного умнее меня поняли, что вы можете делать
+всё то же самое с матричной математикой. Для 2D мы используем матрицу 3x3.
+Матрица 3x3 похожа на сетку с 9 ячейками:
+
+
+
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
+
+Для выполнения математики мы умножаем позицию вниз по столбцам матрицы
+и складываем результаты. Наши позиции имеют только 2 значения, x и y, но
+для выполнения этой математики нам нужно 3 значения, поэтому мы будем использовать 1 для третьего значения.
+
+В этом случае наш результат будет
+
+
+
+
newX =
x *
1.0
+
newY =
x *
2.0
+
extra =
x *
3.0
+
+
y *
4.0
+
y *
5.0
+
y *
6.0
+
+
1 *
7.0
1 *
8.0
1 *
9.0
+
+Вы, вероятно, смотрите на это и думаете "В ЧЕМ СМЫСЛ?" Ну,
+давайте предположим, что у нас есть перемещение. Мы назовем количество, на которое мы хотим
+переместить, tx и ty. Давайте сделаем матрицу так:
+
+
1.0
0.0
0.0
0.0
1.0
0.0
tx
ty
1.0
+
+И теперь посмотрите
+
+
+
+
newX =
x
*
1.0
+
newY =
x
*
0.0
+
extra =
x
*
0.0
+
y
*
0.0
+
y
*
1.0
+
y
*
0.0
+
+
1
*
tx
1
*
ty
1
*
1.0
+
+Если вы помните свою алгебру, мы можем удалить любое место, которое умножается
+на ноль. Умножение на 1 эффективно ничего не делает, поэтому давайте упростим,
+чтобы увидеть, что происходит
+
+
+
+
newX =
x
*
1.0
+
newY =
x
*
0.0
+
extra =
x
*
0.0
+
+
y
*
0.0
+
y
*
1.0
+
y
*
0.0
+
+
1
*
tx
1
*
ty
1
*
1.0
+
+или более кратко
+
+
+newX = x + tx;
+newY = y + ty;
+
+
+И extra нас не очень волнует. Это выглядит удивительно похоже на
+[код перемещения из нашего примера перемещения](webgl-2d-translation.html).
+
+Аналогично давайте сделаем поворот. Как мы указали в посте о повороте,
+нам просто нужны синус и косинус угла, на который мы хотим повернуть, поэтому
+
+
+
+Зачеркивая все умножения на 0 и 1, мы получаем
+
+
+
+
newX =
x
*
c
+
newY =
x
*
-s
+
extra =
x
*
0.0
+
+
y
*
s
+
y
*
c
+
y
*
0.0
+
+
1
*
0.0
1
*
0.0
1
*
1.0
+
+И упрощая, мы получаем
+
+
+newX = x * c + y * s;
+newY = x * -s + y * c;
+
+
+Что точно то же, что у нас было в [примере поворота](webgl-2d-rotation.html).
+
+И наконец масштабирование. Мы назовем наши 2 фактора масштабирования sx и sy
+
+И мы строим матрицу так
+
+
sx
0.0
0.0
0.0
sy
0.0
0.0
0.0
1.0
+
+Применяя матрицу, мы получаем это
+
+
+
+
newX =
x
*
sx
+
newY =
x
*
0.0
+
extra =
x
*
0.0
+
+
y
*
0.0
+
y
*
sy
+
y
*
0.0
+
+
1
*
0.0
1
*
0.0
1
*
1.0
+
+что на самом деле
+
+
+
+
newX =
x
*
sx
+
newY =
x
*
0.0
+
extra =
x
*
0.0
+
+
y
*
0.0
+
y
*
sy
+
y
*
0.0
+
+
1
*
0.0
1
*
0.0
1
*
1.0
+
+что упрощенно
+
+
+newX = x * sx;
+newY = y * sy;
+
+
+Что то же самое, что наш [пример масштабирования](webgl-2d-scale.html).
+
+Теперь я уверен, что вы все еще можете думать "И что? В чем смысл?"
+Это кажется большой работой только для того, чтобы делать то же самое, что мы уже делали.
+
+Здесь вступает в игру магия. Оказывается, мы можем умножать матрицы
+вместе и применять все преобразования сразу. Давайте предположим, что у нас есть
+функция `m3.multiply`, которая берет две матрицы, умножает их и
+возвращает результат.
+
+```js
+var m3 = {
+ multiply: function(a, b) {
+ var a00 = a[0 * 3 + 0];
+ var a01 = a[0 * 3 + 1];
+ var a02 = a[0 * 3 + 2];
+ var a10 = a[1 * 3 + 0];
+ var a11 = a[1 * 3 + 1];
+ var a12 = a[1 * 3 + 2];
+ var a20 = a[2 * 3 + 0];
+ var a21 = a[2 * 3 + 1];
+ var a22 = a[2 * 3 + 2];
+ var b00 = b[0 * 3 + 0];
+ var b01 = b[0 * 3 + 1];
+ var b02 = b[0 * 3 + 2];
+ var b10 = b[1 * 3 + 0];
+ var b11 = b[1 * 3 + 1];
+ var b12 = b[1 * 3 + 2];
+ var b20 = b[2 * 3 + 0];
+ var b21 = b[2 * 3 + 1];
+ var b22 = b[2 * 3 + 2];
+
+ return [
+ b00 * a00 + b01 * a10 + b02 * a20,
+ b00 * a01 + b01 * a11 + b02 * a21,
+ b00 * a02 + b01 * a12 + b02 * a22,
+ b10 * a00 + b11 * a10 + b12 * a20,
+```
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-matrix-stack.md b/webgl/lessons/ru/webgl-2d-matrix-stack.md
new file mode 100644
index 000000000..b1830e35c
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-matrix-stack.md
@@ -0,0 +1,333 @@
+Title: WebGL2 Реализация Стекла Матриц
+Description: Как реализовать функции translate/rotate/scale из Canvas 2D в WebGL
+TOC: 2D - Стек Матриц
+
+
+Эта статья является продолжением [WebGL 2D DrawImage](webgl-2d-drawimage.html).
+Если вы не читали её, я рекомендую [начать оттуда](webgl-2d-drawimage.html).
+
+В той последней статье мы реализовали WebGL эквивалент функции `drawImage` из Canvas 2D,
+включая её способность указывать как исходный прямоугольник, так и прямоугольник назначения.
+
+Что мы ещё не сделали - это позволить нам вращать и/или масштабировать изображение из любой произвольной точки. Мы могли бы сделать это, добавив больше аргументов, как минимум нам нужно было бы указать центральную точку, поворот и масштаб по x и y.
+К счастью, есть более универсальный и полезный способ. Способ, которым Canvas 2D API делает это - это стек матриц.
+Функции стека матриц Canvas 2D API: `save`, `restore`, `translate`, `rotate` и `scale`.
+
+Стек матриц довольно прост в реализации. Мы создаём стек матриц. Мы создаём функции для
+умножения верхней матрицы стека на матрицу перевода, поворота или масштабирования,
+[используя функции, которые мы создали ранее](webgl-2d-matrices.html).
+
+Вот реализация
+
+Сначала конструктор и функции `save` и `restore`
+
+```
+function MatrixStack() {
+ this.stack = [];
+
+ // поскольку стек пуст, это поместит начальную матрицу в него
+ this.restore();
+}
+
+// Извлекает верхний элемент стека, восстанавливая ранее сохранённую матрицу
+MatrixStack.prototype.restore = function() {
+ this.stack.pop();
+ // Никогда не позволяем стеку быть полностью пустым
+ if (this.stack.length < 1) {
+ this.stack[0] = m4.identity();
+ }
+};
+
+// Помещает копию текущей матрицы в стек
+MatrixStack.prototype.save = function() {
+ this.stack.push(this.getCurrentMatrix());
+};
+```
+
+Нам также нужны функции для получения и установки верхней матрицы
+
+```
+// Получает копию текущей матрицы (верх стека)
+MatrixStack.prototype.getCurrentMatrix = function() {
+ return this.stack[this.stack.length - 1].slice(); // создаёт копию
+};
+
+// Позволяет нам установить текущую матрицу
+MatrixStack.prototype.setCurrentMatrix = function(m) {
+ return this.stack[this.stack.length - 1] = m;
+};
+
+```
+
+Наконец, нам нужно реализовать `translate`, `rotate` и `scale`, используя наши
+предыдущие матричные функции.
+
+```
+// Переводит текущую матрицу
+MatrixStack.prototype.translate = function(x, y, z) {
+ if (z === undefined) {
+ z = 0;
+ }
+ var m = this.getCurrentMatrix();
+ this.setCurrentMatrix(m4.translate(m, x, y, z));
+};
+
+// Вращает текущую матрицу вокруг Z
+MatrixStack.prototype.rotateZ = function(angleInRadians) {
+ var m = this.getCurrentMatrix();
+ this.setCurrentMatrix(m4.zRotate(m, angleInRadians));
+};
+
+// Масштабирует текущую матрицу
+MatrixStack.prototype.scale = function(x, y, z) {
+ if (z === undefined) {
+ z = 1;
+ }
+ var m = this.getCurrentMatrix();
+ this.setCurrentMatrix(m4.scale(m, x, y, z));
+};
+```
+
+Обратите внимание, что мы используем 3D матричные математические функции. Мы могли бы просто использовать `0` для `z` при переводе и `1`
+для `z` при масштабировании, но я обнаружил, что я так привык использовать 2D функции из Canvas 2D,
+что часто забываю указать `z`, и тогда код ломается, поэтому давайте сделаем `z` необязательным
+
+```
+// Переводит текущую матрицу
+MatrixStack.prototype.translate = function(x, y, z) {
+ if (z === undefined) {
+ z = 0;
+ }
+ var m = this.getCurrentMatrix();
+ this.setCurrentMatrix(m4.translate(m, x, y, z));
+};
+
+...
+
+// Масштабирует текущую матрицу
+MatrixStack.prototype.scale = function(x, y, z) {
+ if (z === undefined) {
+ z = 1;
+ }
+ var m = this.getCurrentMatrix();
+ this.setCurrentMatrix(m4.scale(m, x, y, z));
+};
+```
+
+Используя наш [`drawImage` из предыдущего урока](webgl-2d-drawimage.html), у нас были эти строки
+
+```
+// эта матрица будет конвертировать из пикселей в пространство отсечения
+var matrix = m4.orthographic(
+ 0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
+
+// переводим наш четырёхугольник в dstX, dstY
+matrix = m4.translate(matrix, dstX, dstY, 0);
+
+// масштабируем наш четырёхугольник размером в 1 единицу
+// от 1 единицы до dstWidth, dstHeight единиц
+matrix = m4.scale(matrix, dstWidth, dstHeight, 1);
+```
+
+Нам просто нужно создать стек матриц
+
+```
+var matrixStack = new MatrixStack();
+```
+
+и умножить на верхнюю матрицу из нашего стека в
+
+```
+// эта матрица будет конвертировать из пикселей в пространство отсечения
+var matrix = m4.orthographic(
+ 0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);
+
+// Стек матриц находится в пикселях, поэтому он идёт после проекции
+// выше, которая конвертировала наше пространство из пространства отсечения в пространство пикселей
+matrix = m4.multiply(matrix, matrixStack.getCurrentMatrix());
+
+// переводим наш четырёхугольник в dstX, dstY
+matrix = m4.translate(matrix, dstX, dstY, 0);
+
+// масштабируем наш четырёхугольник размером в 1 единицу
+// от 1 единицы до dstWidth, dstHeight единиц
+matrix = m4.scale(matrix, dstWidth, dstHeight, 1);
+```
+
+И теперь мы можем использовать это так же, как мы использовали бы это с Canvas 2D API.
+
+Если вы не знаете, как использовать стек матриц, вы можете думать об этом как о
+перемещении и ориентации начала координат холста. Так, например, по умолчанию в 2D холсте начало координат (0,0)
+находится в левом верхнем углу.
+
+Например, если мы переместим начало координат в центр холста, то рисование изображения в точке 0,0
+будет рисовать его, начиная с центра холста
+
+Давайте возьмём [наш предыдущий пример](webgl-2d-drawimage.html) и просто нарисуем одно изображение
+
+```
+var textureInfo = loadImageAndCreateTextureInfo('resources/star.jpg');
+
+function draw(time) {
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ matrixStack.save();
+ matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
+ matrixStack.rotateZ(time);
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+ matrixStack.restore();
+}
+```
+
+И вот это.
+
+{{{example url="../webgl-2d-matrixstack-01.html" }}}
+
+вы можете видеть, что хотя мы передаём `0, 0` в `drawImage`, поскольку мы используем
+`matrixStack.translate` для перемещения начала координат в центр холста,
+изображение рисуется и вращается вокруг этого центра.
+
+Давайте переместим центр вращения в центр изображения
+
+```
+matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
+matrixStack.rotateZ(time);
+matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);
+```
+
+И теперь оно вращается вокруг центра изображения в центре холста
+
+{{{example url="../webgl-2d-matrixstack-02.html" }}}
+
+Давайте нарисуем то же изображение в каждом углу, вращаясь на разных углах
+
+```
+matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
+matrixStack.rotateZ(time);
+
+matrixStack.save();
+{
+ matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+}
+matrixStack.restore();
+
+matrixStack.save();
+{
+ // Мы находимся в центре центрального изображения, поэтому переходим в левый верхний угол
+ matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);
+ matrixStack.rotateZ(Math.sin(time * 2.2));
+ matrixStack.scale(0.2, 0.2);
+ // Теперь мы хотим правый нижний угол изображения, которое мы собираемся нарисовать
+ matrixStack.translate(-textureInfo.width, -textureInfo.height);
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+}
+matrixStack.restore();
+
+matrixStack.save();
+{
+ // Мы находимся в центре центрального изображения, поэтому переходим в правый верхний угол
+ matrixStack.translate(textureInfo.width / 2, textureInfo.height / -2);
+ matrixStack.rotateZ(Math.sin(time * 2.3));
+ matrixStack.scale(0.2, 0.2);
+ // Теперь мы хотим левый нижний угол изображения, которое мы собираемся нарисовать
+ matrixStack.translate(0, -textureInfo.height);
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+}
+matrixStack.restore();
+
+matrixStack.save();
+{
+ // Мы находимся в центре центрального изображения, поэтому переходим в левый нижний угол
+ matrixStack.translate(textureInfo.width / -2, textureInfo.height / 2);
+ matrixStack.rotateZ(Math.sin(time * 2.4));
+ matrixStack.scale(0.2, 0.2);
+ // Теперь мы хотим правый верхний угол изображения, которое мы собираемся нарисовать
+ matrixStack.translate(-textureInfo.width, 0);
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+}
+matrixStack.restore();
+
+matrixStack.save();
+{
+ // Мы находимся в центре центрального изображения, поэтому переходим в правый нижний угол
+ matrixStack.translate(textureInfo.width / 2, textureInfo.height / 2);
+ matrixStack.rotateZ(Math.sin(time * 2.5));
+ matrixStack.scale(0.2, 0.2);
+ // Теперь мы хотим левый верхний угол изображения, которое мы собираемся нарисовать
+ matrixStack.translate(0, 0); // 0,0 означает, что эта строка на самом деле ничего не делает
+
+ drawImage(
+ textureInfo.texture,
+ textureInfo.width,
+ textureInfo.height,
+ 0, 0);
+
+}
+matrixStack.restore();
+```
+
+И вот это
+
+{{{example url="../webgl-2d-matrixstack-03.html" }}}
+
+Если вы думаете о различных функциях стека матриц, `translate`, `rotateZ` и `scale`
+как о перемещении начала координат, то способ, которым я думаю об установке центра вращения, это
+*куда мне нужно переместить начало координат, чтобы когда я вызываю drawImage, определённая часть
+изображения была **в** предыдущем начале координат?*
+
+Другими словами, допустим, на холсте 400x300 я вызываю `matrixStack.translate(210, 150)`.
+В этот момент начало координат находится в точке 210, 150, и всё рисование будет относительно этой точки.
+Если мы вызовем `drawImage` с `0, 0`, это то место, где будет нарисовано изображение.
+
+
+
+Допустим, мы хотим, чтобы центром вращения был правый нижний угол. В этом случае
+куда нам нужно переместить начало координат, чтобы когда мы вызываем `drawImage`,
+точка, которую мы хотим сделать центром вращения, была в текущем начале координат?
+Для правого нижнего угла текстуры это было бы `-textureWidth, -textureHeight`,
+так что теперь когда мы вызываем `drawImage` с `0, 0`, текстура будет нарисована здесь,
+и её правый нижний угол находится в предыдущем начале координат.
+
+
+
+В любой момент то, что мы делали до этого в стеке матриц, не имеет значения. Мы сделали кучу
+вещей, чтобы переместить или масштабировать или повернуть начало координат, но прямо перед тем, как мы вызываем
+`drawImage`, где бы ни находилось начало координат в данный момент, это не имеет значения.
+Это новое начало координат, поэтому нам просто нужно решить, куда переместить это начало координат
+относительно того места, где текстура была бы нарисована, если бы у нас ничего не было перед ней в стеке.
+
+Вы можете заметить, что стек матриц очень похож на [граф сцены, который мы
+рассматривали ранее](webgl-scene-graph.html). Граф сцены имел дерево узлов,
+и когда мы проходили по дереву, мы умножали каждый узел на узел его родителя.
+Стек матриц - это эффективно другая версия того же процесса.
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-rotation.md b/webgl/lessons/ru/webgl-2d-rotation.md
new file mode 100644
index 000000000..9761f5067
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-rotation.md
@@ -0,0 +1,223 @@
+Title: WebGL2 2D Вращение
+Description: Как выполнять вращение в 2D
+TOC: 2D Вращение
+
+Этот пост является продолжением серии постов о WebGL. Первый
+[начался с основ](webgl-fundamentals.html), а предыдущий был
+[о трансляции геометрии](webgl-2d-translation.html).
+
+Я сразу признаюсь, что не знаю, будет ли то, как я объясняю это,
+иметь смысл, но черт с ним, стоит попробовать.
+
+Сначала я хочу познакомить вас с тем, что называется "единичной окружностью". Если вы
+помните математику средней школы (не засыпайте на мне!) окружность
+имеет радиус. Радиус окружности - это расстояние от центра
+окружности до края. Единичная окружность - это окружность с радиусом 1.0.
+
+Вот единичная окружность.
+
+{{{diagram url="../unit-circle.html" width="300" height="300" className="invertdark" }}}
+
+Обратите внимание, как вы перетаскиваете синюю ручку вокруг окружности, позиции X и Y
+изменяются. Они представляют позицию этой точки на окружности. Вверху
+Y равен 1, а X равен 0. Справа X равен 1, а Y равен 0.
+
+Если вы помните из базовой математики 3-го класса, если вы умножаете что-то на 1,
+оно остается тем же. Так что 123 * 1 = 123. Довольно просто, верно? Ну, единичная окружность,
+окружность с радиусом 1.0, также является формой 1. Это вращающаяся 1.
+Так что вы можете умножить что-то на эту единичную окружность, и в некотором смысле это как
+умножение на 1, за исключением того, что происходит магия и вещи вращаются.
+
+Мы возьмем эти значения X и Y из любой точки на единичной окружности
+и умножим нашу геометрию на них из [нашего предыдущего примера](webgl-2d-translation.html).
+
+Вот обновления нашего шейдера.
+
+```
+#version 300 es
+
+in vec2 a_position;
+
+uniform vec2 u_resolution;
+uniform vec2 u_translation;
+uniform vec2 u_rotation;
+
+void main() {
+ // Вращаем позицию
+ vec2 rotatedPosition = vec2(
+ a_position.x * u_rotation.y + a_position.y * u_rotation.x,
+ a_position.y * u_rotation.y - a_position.x * u_rotation.x);
+
+ // Добавляем трансляцию.
+ vec2 position = rotatedPosition + u_translation;
+
+ // конвертируем позицию из пикселей в 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);
+}
+```
+
+И мы обновляем JavaScript, чтобы мы могли передать эти 2 значения.
+
+```
+ ...
+
+ var rotationLocation = gl.getUniformLocation(program, "u_rotation");
+
+ ...
+
+ var rotation = [0, 1];
+
+ ...
+
+ // Рисуем сцену.
+ function drawScene() {
+ webglUtils.resizeCanvasToDisplaySize(gl.canvas);
+
+ // Говорим WebGL, как конвертировать из clip space в пиксели
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+
+ // Очищаем canvas
+ gl.clearColor(0, 0, 0, 0);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ // Говорим использовать нашу программу (пару шейдеров)
+ gl.useProgram(program);
+
+ // Привязываем набор атрибутов/буферов, который мы хотим.
+ gl.bindVertexArray(vao);
+
+ // Передаем разрешение canvas, чтобы мы могли конвертировать из
+ // пикселей в clip space в шейдере
+ gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
+
+ // Устанавливаем цвет.
+ gl.uniform4fv(colorLocation, color);
+
+ // Устанавливаем трансляцию.
+ gl.uniform2fv(translationLocation, translation);
+
+ // Устанавливаем вращение.
+ gl.uniform2fv(rotationLocation, rotation);
+
+ // Рисуем прямоугольник.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+ var count = 18;
+ gl.drawArrays(primitiveType, offset, count);
+ }
+```
+
+И вот результат. Перетащите ручку на окружности, чтобы вращать,
+или слайдеры, чтобы трансформировать.
+
+{{{example url="../webgl-2d-geometry-rotation.html" }}}
+
+Почему это работает? Ну, посмотрите на математику.
+
+
+
+Допустим, у вас есть прямоугольник, и вы хотите его вращать.
+Прежде чем вы начнете его вращать, верхний правый угол находится в точке 3.0, 9.0.
+Давайте выберем точку на единичной окружности на 30 градусов по часовой стрелке от 12 часов.
+
+
+
+Позиция на окружности там 0.50 и 0.87
+
+
+
+Вы можете видеть, что когда мы вращаем эту точку по часовой стрелке вправо, значение X
+становится больше, а Y становится меньше. Если бы мы продолжали за 90 градусов,
+X снова начал бы становиться меньше, а Y начал бы становиться больше.
+Этот паттерн дает нам вращение.
+
+Есть другое название для точек на единичной окружности. Они называются
+синус и косинус. Так что для любого заданного угла мы можем просто посмотреть
+синус и косинус, как это.
+
+```
+function printSineAndCosineForAnAngle(angleInDegrees) {
+ var angleInRadians = angleInDegrees * Math.PI / 180;
+ var s = Math.sin(angleInRadians);
+ var c = Math.cos(angleInRadians);
+ console.log("s = " + s + " c = " + c);
+}
+```
+
+Если вы скопируете и вставите код в консоль JavaScript и
+напишете `printSineAndCosignForAngle(30)`, вы увидите, что он выводит
+`s = 0.49 c = 0.87` (примечание: я округлил числа.)
+
+Если вы все это сложите вместе, вы можете вращать вашу геометрию на любой угол,
+который вы желаете. Просто установите вращение на синус и косинус угла,
+на который вы хотите вращать.
+
+ ...
+ var angleInRadians = angleInDegrees * Math.PI / 180;
+ rotation[0] = Math.sin(angleInRadians);
+ rotation[1] = Math.cos(angleInRadians);
+
+Вот версия, которая просто имеет настройку угла. Перетащите слайдеры,
+чтобы трансформировать или вращать.
+
+{{{example url="../webgl-2d-geometry-rotation-angle.html" }}}
+
+Я надеюсь, что это имело некоторый смысл. [Следующий более простой. Масштабирование](webgl-2d-scale.html).
+
+
Что такое радианы?
+
+Радианы - это единица измерения, используемая с окружностями, вращением и углами.
+Так же, как мы можем измерять расстояние в дюймах, ярдах, метрах и т.д., мы можем
+измерять углы в градусах или радианах.
+
+
+Вы, вероятно, знаете, что математика с метрическими измерениями проще, чем
+математика с имперскими измерениями. Чтобы перейти от дюймов к футам, мы делим на 12.
+Чтобы перейти от дюймов к ярдам, мы делим на 36. Я не знаю о вас, но я
+не могу делить на 36 в уме. С метрической системой это намного проще. Чтобы перейти от
+миллиметров к сантиметрам, мы делим на 10. Чтобы перейти от миллиметров к метрам,
+мы делим на 1000. Я **могу** делить на 1000 в уме.
+
+
+Радианы против градусов похожи. Градусы делают математику сложной. Радианы делают
+математику простой. В окружности 360 градусов, но только 2π радиан.
+Так что полный оборот - это 2π радиан. Половина оборота - это 1π радиан. Четверть оборота, т.е. 90 градусов,
+это 1/2π радиан. Так что если вы хотите вращать что-то на 90 градусов, просто используйте
+Math.PI * 0.5. Если вы хотите вращать это на 45 градусов, используйте
+Math.PI * 0.25 и т.д.
+
+
+Почти вся математика, связанная с углами, окружностями или вращением, работает очень просто,
+если вы начинаете думать в радианах. Так что попробуйте. Используйте радианы, а не градусы,
+кроме отображений в пользовательском интерфейсе.
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-scale.md b/webgl/lessons/ru/webgl-2d-scale.md
new file mode 100644
index 000000000..08fa9e1e1
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-scale.md
@@ -0,0 +1,137 @@
+Title: WebGL2 2D Масштабирование
+Description: Как выполнять масштабирование в 2D
+TOC: 2D Масштабирование
+
+Этот пост является продолжением серии постов о WebGL.
+Первый [начался с основ](webgl-fundamentals.html), а
+предыдущий был [о вращении геометрии](webgl-2d-rotation.html).
+
+Масштабирование так же [просто, как трансляция](webgl-2d-translation.html).
+
+Мы умножаем позицию на наш желаемый масштаб. Вот изменения
+из [нашего предыдущего примера](webgl-2d-rotation.html).
+
+```
+#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;
+
+ // конвертируем позицию из пикселей в 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);
+}
+```
+
+и мы добавляем JavaScript, необходимый для установки масштаба, когда мы рисуем.
+
+```
+ ...
+
+ var scaleLocation = gl.getUniformLocation(program, "u_scale");
+
+ ...
+
+ var scale = [1, 1];
+
+
+ // Рисуем сцену.
+ function drawScene() {
+ webglUtils.resizeCanvasToDisplaySize(gl.canvas);
+
+ // Говорим WebGL, как конвертировать из clip space в пиксели
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+
+ // Очищаем canvas
+ gl.clearColor(0, 0, 0, 0);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ // Говорим использовать нашу программу (пару шейдеров)
+ gl.useProgram(program);
+
+ // Привязываем набор атрибутов/буферов, который мы хотим.
+ gl.bindVertexArray(vao);
+
+ // Передаем разрешение canvas, чтобы мы могли конвертировать из
+ // пикселей в clip space в шейдере
+ gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
+
+ // Устанавливаем цвет.
+ gl.uniform4fv(colorLocation, color);
+
+ // Устанавливаем трансляцию.
+ gl.uniform2fv(translationLocation, translation);
+
+ // Устанавливаем вращение.
+ gl.uniform2fv(rotationLocation, rotation);
+
+ // Устанавливаем масштаб.
+ gl.uniform2fv(scaleLocation, scale);
+
+ // Рисуем прямоугольник.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+ var count = 18;
+ gl.drawArrays(primitiveType, offset, count);
+ }
+```
+
+И теперь у нас есть масштабирование. Перетащите слайдеры.
+
+{{{example url="../webgl-2d-geometry-scale.html" }}}
+
+Одна вещь, которую стоит заметить, это то, что масштабирование отрицательным значением переворачивает нашу геометрию.
+
+Другая вещь, которую стоит заметить, это то, что она масштабируется от 0, 0, что для нашей F является
+верхним левым углом. Это имеет смысл, поскольку мы умножаем позиции
+на масштаб, они будут двигаться от 0, 0. Вы, вероятно,
+можете представить способы исправить это. Например, вы могли бы добавить другую трансляцию
+перед масштабированием, *предварительную* трансляцию масштабирования. Другое решение было бы
+изменить фактические данные позиции F. Мы скоро рассмотрим другой способ.
+
+Я надеюсь, что эти последние 3 поста были полезны для понимания
+[трансляции](webgl-2d-translation.html), [вращения](webgl-2d-rotation.html)
+и масштабирования. Далее мы рассмотрим [магию матриц](webgl-2d-matrices.html),
+которая объединяет все 3 из них в гораздо более простую и часто более полезную форму.
+
+
+
Почему 'F'?
+
+В первый раз я увидел, как кто-то использует 'F', было на текстуре.
+Сама 'F' не важна. Важно то, что
+вы можете определить ее ориентацию с любого направления. Если бы мы
+использовали сердце ❤ или треугольник △, например, мы не могли бы
+сказать, перевернут ли он горизонтально. Круг ○ был бы
+еще хуже. Цветной прямоугольник, возможно, работал бы с
+разными цветами на каждом углу, но тогда вам пришлось бы помнить,
+какой угол был каким. Ориентация F мгновенно узнаваема.
+
+
+
+Любая форма, ориентацию которой вы можете определить, подойдет,
+я просто использовал 'F' с тех пор, как впервые познакомился с этой идеей.
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-translation.md b/webgl/lessons/ru/webgl-2d-translation.md
new file mode 100644
index 000000000..bea43f185
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-translation.md
@@ -0,0 +1,261 @@
+Title: WebGL2 2D Трансляция
+Description: Как выполнять трансляцию в 2D
+TOC: 2D Трансляция
+
+Прежде чем мы перейдем к 3D, давайте еще немного останемся в 2D.
+Пожалуйста, потерпите меня. Эта статья может показаться чрезвычайно очевидной
+некоторым, но я подведу к определенной точке в нескольких статьях.
+
+Эта статья является продолжением серии, начинающейся с
+[Основ WebGL](webgl-fundamentals.html). Если вы их не читали,
+я предлагаю прочитать хотя бы первую, а затем вернуться сюда.
+
+Трансляция - это какое-то модное математическое название, которое в основном означает "перемещать"
+что-то. Я полагаю, что перевод предложения с английского на японский тоже подходит,
+но в данном случае мы говорим о перемещении геометрии. Используя
+пример кода, с которым мы закончили в [первой статье](webgl-fundamentals.html),
+вы могли бы легко трансформировать наш прямоугольник, просто изменив значения,
+передаваемые в `setRectangle`, верно? Вот пример, основанный на нашем
+[предыдущем примере](webgl-fundamentals.html).
+
+```
+ // Сначала давайте создадим некоторые переменные
+ // для хранения трансляции, ширины и высоты прямоугольника
+ var translation = [0, 0];
+ var width = 100;
+ var height = 30;
+ var color = [Math.random(), Math.random(), Math.random(), 1];
+
+ // Затем давайте создадим функцию для
+ // перерисовки всего. Мы можем вызвать эту
+ // функцию после того, как обновим трансляцию.
+
+ // Рисуем сцену.
+ function drawScene() {
+ webglUtils.resizeCanvasToDisplaySize(gl.canvas);
+
+ // Говорим WebGL, как конвертировать из clip space в пиксели
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+
+ // Очищаем canvas
+ gl.clearColor(0, 0, 0, 0);
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ // Говорим использовать нашу программу (пару шейдеров)
+ gl.useProgram(program);
+
+ // Привязываем набор атрибутов/буферов, который мы хотим.
+ gl.bindVertexArray(vao);
+
+ // Передаем разрешение canvas, чтобы мы могли конвертировать из
+ // пикселей в clip space в шейдере
+ gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
+
+ // Обновляем буфер позиций с позициями прямоугольника
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ setRectangle(gl, translation[0], translation[1], width, height);
+
+ // Устанавливаем цвет.
+ gl.uniform4fv(colorLocation, color);
+
+ // Рисуем прямоугольник.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+ var count = 6;
+ gl.drawArrays(primitiveType, offset, count);
+ }
+```
+
+В примере ниже я добавил пару слайдеров, которые будут обновлять
+`translation[0]` и `translation[1]` и вызывать `drawScene` каждый раз, когда они изменяются.
+Перетащите слайдеры, чтобы трансформировать прямоугольник.
+
+{{{example url="../webgl-2d-rectangle-translate.html" }}}
+
+Пока все хорошо. Но теперь представьте, что мы хотели бы сделать то же самое с
+более сложной формой.
+
+Допустим, мы хотели бы нарисовать букву 'F', которая состоит из 6 треугольников, как эта.
+
+
+
+Ну, следуя нашему текущему коду, нам пришлось бы изменить `setRectangle`
+на что-то более похожее на это.
+
+```
+// Заполняем текущий буфер ARRAY_BUFFER значениями, которые определяют букву 'F'.
+function setGeometry(gl, x, y) {
+ var width = 100;
+ var height = 150;
+ var thickness = 30;
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array([
+ // левая колонка
+ x, y,
+ x + thickness, y,
+ x, y + height,
+ x, y + height,
+ x + thickness, y,
+ x + thickness, y + height,
+
+ // верхняя перекладина
+ x + thickness, y,
+ x + width, y,
+ x + thickness, y + thickness,
+ x + thickness, y + thickness,
+ x + width, y,
+ x + width, y + thickness,
+
+ // средняя перекладина
+ x + thickness, y + thickness * 2,
+ x + width * 2 / 3, y + thickness * 2,
+ x + thickness, y + thickness * 3,
+ x + thickness, y + thickness * 3,
+ x + width * 2 / 3, y + thickness * 2,
+ x + width * 2 / 3, y + thickness * 3]),
+ gl.STATIC_DRAW);
+}
+```
+
+Вы, надеюсь, видите, что это не будет хорошо масштабироваться. Если мы хотим
+нарисовать какую-то очень сложную геометрию с сотнями или тысячами линий, нам
+пришлось бы написать довольно сложный код. Кроме того, каждый раз, когда мы
+рисуем, JavaScript должен обновлять все точки.
+
+Есть более простой способ. Просто загрузите геометрию и выполните трансляцию
+в шейдере.
+
+Вот новый шейдер
+
+```
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec2 a_position;
+
+// Используется для передачи разрешения canvas
+uniform vec2 u_resolution;
+
+// трансляция для добавления к позиции
+uniform vec2 u_translation;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Добавляем трансляцию
+ vec2 position = a_position + u_translation;
+
+ // конвертируем позицию из пикселей в 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);
+}
+```
+
+и мы немного реструктурируем код. Во-первых, нам нужно установить
+геометрию только один раз.
+
+```
+// Заполняем текущий буфер ARRAY_BUFFER
+// значениями, которые определяют букву 'F'.
+function setGeometry(gl) {
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array([
+ // левая колонка
+ 0, 0,
+ 30, 0,
+ 0, 150,
+ 0, 150,
+ 30, 0,
+ 30, 150,
+
+ // верхняя перекладина
+ 30, 0,
+ 100, 0,
+ 30, 30,
+ 30, 30,
+ 100, 0,
+ 100, 30,
+
+ // средняя перекладина
+ 30, 60,
+ 67, 60,
+ 30, 90,
+ 30, 90,
+ 67, 60,
+ 67, 90]),
+ gl.STATIC_DRAW);
+}
+```
+
+Затем нам просто нужно обновить `u_translation` перед тем, как мы рисуем, с
+трансляцией, которую мы желаем.
+
+```
+ ...
+
+ var translationLocation = gl.getUniformLocation(
+ program, "u_translation");
+
+ ...
+
+ // Устанавливаем геометрию.
+ setGeometry(gl);
+
+ ...
+
+ // Рисуем сцену.
+ function drawScene() {
+ webglUtils.resizeCanvasToDisplaySize(gl.canvas);
+
+ // Говорим WebGL, как конвертировать из clip space в пиксели
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+
+ // Говорим использовать нашу программу (пару шейдеров)
+ gl.useProgram(program);
+
+ // Привязываем набор атрибутов/буферов, который мы хотим.
+ gl.bindVertexArray(vao);
+
+ // Передаем разрешение canvas, чтобы мы могли конвертировать из
+ // пикселей в clip space в шейдере
+ gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
+
+ // Устанавливаем цвет.
+ gl.uniform4fv(colorLocation, color);
+
+ // Устанавливаем трансляцию.
+ gl.uniform2fv(translationLocation, translation);
+
+ // Рисуем прямоугольник.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+ var count = 18;
+ gl.drawArrays(primitiveType, offset, count);
+ }
+```
+
+Обратите внимание, что `setGeometry` вызывается только один раз. Она больше не находится внутри `drawScene`.
+
+И вот этот пример. Снова перетащите слайдеры, чтобы обновить трансляцию.
+
+{{{example url="../webgl-2d-geometry-translate-better.html" }}}
+
+Теперь, когда мы рисуем, WebGL делает практически все. Все, что мы делаем, это
+устанавливаем трансляцию и просим его нарисовать. Даже если бы наша геометрия имела десятки
+тысяч точек, основной код остался бы тем же.
+
+Если хотите, вы можете сравнить
+версию, которая использует сложный JavaScript
+выше для обновления всех точек.
+
+Я надеюсь, что этот пример был слишком очевиден. В [следующей статье мы перейдем
+к вращению](webgl-2d-rotation.html).
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-2d-vs-3d-library.md b/webgl/lessons/ru/webgl-2d-vs-3d-library.md
new file mode 100644
index 000000000..4cf617a0e
--- /dev/null
+++ b/webgl/lessons/ru/webgl-2d-vs-3d-library.md
@@ -0,0 +1,212 @@
+Title: WebGL2 - Растеризация против 3D библиотек
+Description: Почему WebGL не является 3D библиотекой и почему это важно.
+TOC: 2D против 3D библиотек
+
+
+Этот пост является своего рода побочной темой в серии постов о WebGL.
+Первый [начался с основ](webgl-fundamentals.html)
+
+Я пишу это, потому что моё утверждение, что WebGL - это API растеризации, а не 3D API,
+задевает некоторых людей. Я не уверен, почему они чувствуют угрозу
+или что бы то ни было, что заставляет их так расстраиваться, когда я называю WebGL API растеризации.
+
+Аргументированно всё является вопросом перспективы. Я могу сказать, что нож - это
+столовый прибор, кто-то другой может сказать, что нож - это инструмент, а ещё один
+человек может сказать, что нож - это оружие.
+
+В случае с WebGL, однако, есть причина, по которой я думаю, что важно
+называть WebGL API растеризации, и это конкретно из-за количества знаний 3D
+математики, которые вам нужно знать, чтобы использовать WebGL для рисования чего-либо в 3D.
+
+Я бы утверждал, что всё, что называет себя 3D библиотекой, должно делать
+3D части за вас. Вы должны иметь возможность дать библиотеке некоторые 3D данные,
+некоторые параметры материала, некоторые источники света, и она должна рисовать 3D за вас.
+WebGL (и OpenGL ES 2.0+) оба используются для рисования 3D, но ни один не соответствует этому
+описанию.
+
+Чтобы привести аналогию, C++ не "обрабатывает слова" из коробки. Мы
+не называем C++ "текстовым процессором", даже хотя текстовые процессоры могут быть
+написаны на C++. Аналогично WebGL не рисует 3D графику из коробки.
+Вы можете написать библиотеку, которая будет рисовать 3D графику с WebGL, но сама по себе
+она не делает 3D графику.
+
+Чтобы привести дальнейший пример, предположим, что мы хотим нарисовать куб в 3D
+с источниками света.
+
+Вот код в three.js для отображения этого
+
+
{{#escapehtml}}
+ // Настройка.
+ renderer = new THREE.WebGLRenderer({canvas: document.querySelector("#canvas")});
+ c.appendChild(renderer.domElement);
+
+ // Создаём и настраиваем камеру.
+ camera = new THREE.PerspectiveCamera(70, 1, 1, 1000);
+ camera.position.z = 400;
+
+ // Создаём сцену
+ scene = new THREE.Scene();
+
+ // Создаём куб.
+ var geometry = new THREE.BoxGeometry(200, 200, 200);
+
+ // Создаём материал
+ var material = new THREE.MeshPhongMaterial({
+ ambient: 0x555555,
+ color: 0x555555,
+ specular: 0xffffff,
+ shininess: 50,
+ shading: THREE.SmoothShading
+ });
+
+ // Создаём сетку на основе геометрии и материала
+ mesh = new THREE.Mesh(geometry, material);
+ scene.add(mesh);
+
+ // Добавляем 2 источника света.
+ light1 = new THREE.PointLight(0xff0040, 2, 0);
+ light1.position.set(200, 100, 300);
+ scene.add(light1);
+
+ light2 = new THREE.PointLight(0x0040ff, 2, 0);
+ light2.position.set(-200, 100, 300);
+ scene.add(light2);
+{{/escapehtml}}
+
+и вот это отображается.
+
+{{{example url="resources/three-js-cube-with-lights.html" }}}
+
+Вот аналогичный код в OpenGL (не ES) для отображения куба с 2 источниками света.
+
+
+
+Обратите внимание, как нам почти не нужно знание 3D математики для любого из этих
+примеров. Сравните это с WebGL. Я не собираюсь писать код,
+требуемый для WebGL. Код не намного больше. Дело не в
+количестве требуемых строк. Дело в количестве **знаний**,
+требуемых. В двух 3D библиотеках они заботятся о 3D. Вы даёте им
+позицию камеры и поле зрения, пару источников света и куб. Они
+занимаются всем остальным. Другими словами: Они являются 3D библиотеками.
+
+В WebGL, с другой стороны, вам нужно знать матричную математику, нормализованные
+координаты, усечённые пирамиды, векторные произведения, скалярные произведения, интерполяцию varying, освещение,
+спекулярные вычисления и все виды других вещей, которые часто требуют месяцев
+или лет для полного понимания.
+
+Вся суть 3D библиотеки в том, чтобы иметь эти знания встроенными, чтобы вам
+не нужны были эти знания самим, вы можете просто полагаться на библиотеку, чтобы
+обрабатывать это за вас. Это было верно для оригинального OpenGL, как показано выше.
+Это верно для других 3D библиотек, таких как three.js. Это НЕ верно для OpenGL
+ES 2.0+ или WebGL.
+
+Кажется вводящим в заблуждение называть WebGL 3D библиотекой. Пользователь, приходящий к WebGL,
+подумает "о, 3D библиотека. Круто. Это будет делать 3D за меня" и затем обнаружит
+трудным путём, что нет, это совсем не так.
+
+Мы можем даже пойти на шаг дальше. Вот рисование 3D каркасного
+куба в Canvas.
+
+{{{example url="resources/3d-in-canvas.html" }}}
+
+И вот рисование каркасного куба в WebGL.
+
+{{{example url="resources/3d-in-webgl.html" }}}
+
+Если вы изучите код, вы увидите, что нет большой разницы с точки зрения
+количества знаний или, если на то пошло, даже кода. В конечном счёте
+версия Canvas перебирает вершины, делает математику, КОТОРУЮ МЫ ПОСТАВИЛИ, и
+рисует некоторые линии в 2D. Версия WebGL делает то же самое, кроме того, что математика,
+КОТОРУЮ МЫ ПОСТАВИЛИ, находится в GLSL и выполняется GPU.
+
+Суть этой последней демонстрации в том, чтобы показать, что эффективно WebGL - это
+просто движок растеризации, аналогичный Canvas 2D. Конечно,
+WebGL имеет функции, которые помогают вам реализовать 3D. WebGL имеет буфер глубины,
+который делает сортировку по глубине намного проще, чем система без него. WebGL
+также имеет различные встроенные математические функции, которые очень полезны для выполнения 3D
+математики, хотя аргументированно нет ничего, что делает их 3D. Они - математическая
+библиотека. Вы используете их для математики независимо от того, является ли эта математика 1D, 2D, 3D,
+чем угодно. Но в конечном счёте WebGL только растеризует. Вы должны предоставить ему
+координаты пространства отсечения, которые представляют то, что вы хотите нарисовать. Конечно,
+вы предоставляете x,y,z,w, и он делит на W перед рендерингом, но это
+едва ли достаточно, чтобы квалифицировать WebGL как 3D библиотеку. В 3D библиотеках вы
+поставляете 3D данные, библиотеки заботятся о вычислении точек пространства отсечения из 3D.
+
+Чтобы дать ещё несколько точек отсчёта, [emscripten](https://emscripten.org/)
+предоставляет эмуляцию старого OpenGL поверх WebGL. Этот код находится
+[здесь](https://github.com/emscripten-core/emscripten/blob/main/src/lib/libglemu.js).
+Если вы просмотрите код, вы увидите, что многое из этого генерирует шейдеры для
+эмуляции старых 3D частей OpenGL, которые были удалены в OpenGL ES 2.0. Вы можете
+увидеть то же самое в
+[Regal](https://chromium.googlesource.com/external/p3/regal/+/refs/heads/master/src/regal/RegalIff.cpp),
+проект, который NVidia начала для эмуляции старого OpenGL с включённым 3D в современном OpenGL
+без включённого 3D. Ещё один пример, [вот шейдеры, которые использует three.js](https://gist.github.com/greggman/41d93c00649cba78abdbfc1231c9158c) для
+предоставления 3D. Вы можете видеть, что многое происходит во всех этих примерах.
+Всё это, а также код для поддержки этого, поставляется этими библиотеками,
+а не WebGL.
+
+Я надеюсь, что вы хотя бы понимаете, откуда я исхожу, когда говорю, что WebGL
+не является 3D библиотекой. Я надеюсь, что вы также поймёте, что 3D библиотека должна
+обрабатывать 3D за вас. OpenGL делал это. Three.js делает это. OpenGL ES 2.0 и WebGL
+не делают. Поэтому они аргументированно не принадлежат к той же широкой категории
+"3D библиотек".
+
+Суть всего этого в том, чтобы дать разработчику, который новичок в WebGL,
+понимание WebGL в его основе. Знание того, что WebGL не является
+3D библиотекой и что они должны предоставить все знания сами,
+позволяет им знать, что дальше для них и хотят ли они преследовать
+эти знания 3D математики или вместо этого выбрать 3D библиотеку для обработки этого
+за них. Это также убирает большую часть тайны того, как это работает.
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-camera.md b/webgl/lessons/ru/webgl-3d-camera.md
new file mode 100644
index 000000000..9a4477108
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-camera.md
@@ -0,0 +1,338 @@
+Title: WebGL2 3D - Камеры
+Description: Как работают камеры в WebGL
+TOC: 3D - Камеры
+
+
+Эта статья является продолжением серии статей о WebGL.
+Первая [началась с основ](webgl-fundamentals.html), а
+предыдущая была о [3D перспективной проекции](webgl-3d-perspective.html).
+Если вы их не читали, пожалуйста, сначала ознакомьтесь с ними.
+
+В последней статье нам пришлось переместить F перед усеченной пирамидой, потому что функция `m4.perspective`
+ожидает, что она будет находиться в начале координат (0, 0, 0), и что объекты в усеченной пирамиде находятся от `-zNear`
+до `-zFar` перед ней.
+
+Перемещение вещей перед видом не кажется правильным подходом, не так ли? В реальном мире
+вы обычно перемещаете камеру, чтобы сфотографировать здание.
+
+{{{diagram url="resources/camera-move-camera.html?mode=0" caption="перемещение камеры к объектам" }}}
+
+Вы обычно не перемещаете здания, чтобы они были перед камерой.
+
+{{{diagram url="resources/camera-move-camera.html?mode=1" caption="перемещение объектов к камере" }}}
+
+Но в нашей последней статье мы придумали проекцию, которая требует, чтобы вещи были
+перед началом координат на оси -Z. Чтобы достичь этого, что мы хотим сделать,
+это переместить камеру в начало координат и переместить все остальное на правильное количество,
+чтобы оно все еще было в том же месте *относительно камеры*.
+
+{{{diagram url="resources/camera-move-camera.html?mode=2" caption="перемещение объектов к виду" }}}
+
+Нам нужно эффективно переместить мир перед камерой. Самый простой
+способ сделать это - использовать "обратную" матрицу. Математика для вычисления
+обратной матрицы в общем случае сложна, но концептуально это легко.
+Обратная - это значение, которое вы бы использовали, чтобы отрицать какое-то другое значение. Например,
+обратная матрица, которая перемещает по X на 123, - это матрица, которая
+перемещает по X на -123. Обратная матрица, которая
+масштабирует на 5, - это матрица, которая масштабирует на 1/5 или 0.2. Обратная матрица, которая поворачивает
+на 30° вокруг оси X, была бы той, которая поворачивает на -30° вокруг оси X.
+
+
+До этого момента мы использовали перемещение, поворот и масштабирование, чтобы влиять на
+позицию и ориентацию нашей 'F'. После умножения всех
+матриц вместе у нас есть одна матрица, которая представляет, как переместить
+'F' от начала координат к месту, размеру и ориентации, которые мы хотим. Мы можем
+сделать то же самое для камеры. Как только у нас есть матрица, которая говорит нам, как
+перемещать и поворачивать камеру от начала координат туда, где мы хотим, мы можем
+вычислить ее обратную, которая даст нам матрицу, которая говорит нам, как перемещать
+и поворачивать все остальное в противоположном количестве, что эффективно сделает
+так, что камера будет в (0, 0, 0), и мы переместили все перед
+ней.
+
+Давайте сделаем 3D сцену с кругом 'F', как на диаграммах выше.
+
+Вот код.
+
+```
+function drawScene() {
+ var numFs = 5;
+ var radius = 200;
+
+ ...
+
+ // Вычисляем матрицу
+ var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
+ var zNear = 1;
+ var zFar = 2000;
+ var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
+
+ var cameraMatrix = m4.yRotation(cameraAngleRadians);
+ cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
+
+ // Делаем матрицу вида из матрицы камеры.
+ var viewMatrix = m4.inverse(cameraMatrix);
+
+ // перемещаем проекционное пространство в пространство вида (пространство перед
+ // камерой)
+ var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
+
+ // Рисуем 'F' в круге
+ for (var ii = 0; ii < numFs; ++ii) {
+ var angle = ii * Math.PI * 2 / numFs;
+
+ var x = Math.cos(angle) * radius;
+ var z = Math.sin(angle) * radius;
+ // добавляем перемещение для этой F
+ var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
+
+ // Устанавливаем матрицу.
+ gl.uniformMatrix4fv(matrixLocation, false, matrix);
+
+ // Рисуем геометрию.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+ var count = 16 * 6;
+ gl.drawArrays(primitiveType, offset, count);
+ }
+}
+```
+
+Сразу после того, как мы вычисляем нашу проекционную матрицу, вы можете видеть, что мы вычисляем камеру, которая
+ходит вокруг 'F', как на диаграмме выше.
+
+```
+ // Вычисляем матрицу камеры
+ var cameraMatrix = m4.yRotation(cameraAngleRadians);
+ cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
+```
+
+Затем мы вычисляем "матрицу вида" из матрицы камеры. "Матрица вида"
+- это матрица, которая перемещает все в противоположность камере, эффективно
+делая все относительно камеры, как будто камера была в
+начале координат (0,0,0)
+
+```
+ // Делаем матрицу вида из матрицы камеры.
+ var viewMatrix = m4.inverse(cameraMatrix);
+```
+
+Затем мы комбинируем (умножаем) их, чтобы сделать матрицу viewProjection.
+
+```
+ // создаем матрицу viewProjection. Это будет применять перспективу
+ // И перемещать мир так, что камера эффективно является началом координат
+ var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
+```
+
+Наконец, мы используем это пространство как начальное пространство для размещения каждой `F'
+
+```
+ var x = Math.cos(angle) * radius;
+ var z = Math.sin(angle) * radius;
+ var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
+```
+
+Другими словами, viewProjection одинаков для каждой `F`. Та же перспектива,
+та же камера.
+
+И вуаля! Камера, которая ходит вокруг круга 'F'. Перетащите слайдер `cameraAngle`,
+чтобы переместить камеру вокруг.
+
+{{{example url="../webgl-3d-camera.html" }}}
+
+Это все хорошо, но использование поворота и перемещения для перемещения камеры туда, где вы хотите, и указания на
+то, что вы хотите видеть, не всегда легко. Например, если бы мы хотели, чтобы камера всегда указывала
+на конкретную одну из 'F', потребовалась бы довольно сумасшедшая математика, чтобы вычислить, как повернуть
+камеру, чтобы указать на эту 'F', пока она ходит вокруг круга 'F'.
+
+К счастью, есть более простой способ. Мы можем просто решить, где мы хотим камеру и на что мы хотим, чтобы она указывала,
+и затем вычислить матрицу, которая поместит камеру туда. Основываясь на том, как работают матрицы, это удивительно легко.
+
+Сначала нам нужно знать, где мы хотим камеру. Мы назовем это
+`cameraPosition`. Затем нам нужно знать позицию вещи, на которую мы хотим
+смотреть или целиться. Мы назовем это `target`. Если мы вычтем
+`cameraPosition` из `target`, у нас будет вектор, который указывает в
+направлении, в котором нам нужно идти от камеры, чтобы добраться до цели. Давайте
+назовем это `zAxis`. Поскольку мы знаем, что камера указывает в направлении -Z, мы
+можем вычесть другим способом `cameraPosition - target`. Мы нормализуем
+результаты и копируем их прямо в `z` часть матрицы.
+
+
+
+Эта часть матрицы представляет ось Z. В этом случае ось Z
+камеры. Нормализация вектора означает создание вектора, который представляет
+1.0. Если вы вернетесь к [статье о 2D повороте](webgl-2d-rotation.html), где мы говорили о единичных
+окружностях и о том, как они помогали с 2D поворотом. В 3D нам нужны единичные сферы,
+и нормализованный вектор представляет точку на единичной сфере.
+
+{{{diagram url="resources/cross-product-diagram.html?mode=0" caption="ось z" }}}
+
+Но этого недостаточно информации. Просто один вектор дает нам точку на
+единичной сфере, но какую ориентацию от этой точки использовать для ориентации вещей? Нам
+нужно заполнить другие части матрицы. Конкретно части оси X
+и оси Y. Мы знаем, что в общем эти 3 части перпендикулярны
+друг другу. Мы также знаем, что "в общем" мы не направляем камеру
+прямо вверх. Учитывая это, если мы знаем, какой путь вверх, в этом случае (0,1,0),
+Мы можем использовать это и что-то, называемое "векторным произведением", чтобы вычислить ось X
+и ось Y для матрицы.
+
+Я не имею представления, что означает векторное произведение в математических терминах. Что я
+знаю, так это то, что если у вас есть 2 единичных вектора, и вы вычисляете векторное произведение
+из них, вы получите вектор, который перпендикулярен этим 2 векторам. Другими
+словами, если у вас есть вектор, указывающий на юго-восток, и вектор,
+указывающий вверх, и вы вычисляете векторное произведение, вы получите вектор, указывающий
+либо на юго-запад, либо на северо-восток, поскольку это 2 вектора, которые перпендикулярны
+юго-востоку и вверх. В зависимости от того, в каком порядке вы вычисляете векторное произведение,
+вы получите противоположный ответ.
+
+В любом случае, если мы вычислим векторное произведение нашего `zAxis` и
+`up`, мы получим xAxis для камеры.
+
+{{{diagram url="resources/cross-product-diagram.html?mode=1" caption="up cross zAxis = xAxis" }}}
+
+И теперь, когда у нас есть `xAxis`, мы можем вычислить векторное произведение `zAxis` и `xAxis`,
+что даст нам `yAxis` камеры
+
+{{{diagram url="resources/cross-product-diagram.html?mode=2" caption="zAxis cross xAxis = yAxis"}}}
+
+Теперь все, что нам нужно сделать, это вставить 3 оси в матрицу. Это дает нам
+матрицу, которая будет ориентировать что-то, что указывает на `target` из
+`cameraPosition`. Нам просто нужно добавить `position`
+
+
++----+----+----+----+
+| Xx | Xy | Xz | 0 | <- ось x
++----+----+----+----+
+| Yx | Yy | Yz | 0 | <- ось y
++----+----+----+----+
+| Zx | Zy | Zz | 0 | <- ось z
++----+----+----+----+
+| Tx | Ty | Tz | 1 | <- позиция камеры
++----+----+----+----+
+
+
+Вот код для вычисления векторного произведения 2 векторов.
+
+```
+function cross(a, b) {
+ return [a[1] * b[2] - a[2] * b[1],
+ a[2] * b[0] - a[0] * b[2],
+ a[0] * b[1] - a[1] * b[0]];
+}
+```
+
+Вот код для вычитания двух векторов.
+
+```
+function subtractVectors(a, b) {
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
+}
+```
+
+Вот код для нормализации вектора (превращения его в единичный вектор).
+
+```
+function normalize(v) {
+ var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
+ // убеждаемся, что мы не делим на 0.
+ if (length > 0.00001) {
+ return [v[0] / length, v[1] / length, v[2] / length];
+ } else {
+ return [0, 0, 0];
+ }
+}
+```
+
+Вот код для вычисления матрицы "lookAt".
+
+```
+var m4 = {
+ lookAt: function(cameraPosition, target, up) {
+ var zAxis = normalize(
+ subtractVectors(cameraPosition, target));
+ var xAxis = normalize(cross(up, zAxis));
+ var yAxis = normalize(cross(zAxis, xAxis));
+
+ return [
+ xAxis[0], xAxis[1], xAxis[2], 0,
+ yAxis[0], yAxis[1], yAxis[2], 0,
+ zAxis[0], zAxis[1], zAxis[2], 0,
+ cameraPosition[0],
+ cameraPosition[1],
+ cameraPosition[2],
+ 1,
+ ];
+ },
+```
+
+И вот как мы можем использовать это, чтобы заставить камеру указывать на конкретную 'F',
+когда мы перемещаем ее.
+
+```
+ ...
+
+ // Вычисляем позицию первой F
+ var fPosition = [radius, 0, 0];
+
+ // Используем матричную математику для вычисления позиции на круге.
+ var cameraMatrix = m4.yRotation(cameraAngleRadians);
+ cameraMatrix = m4.translate(cameraMatrix, 0, 50, radius * 1.5);
+
+ // Получаем позицию камеры из матрицы, которую мы вычислили
+ var cameraPosition = [
+ cameraMatrix[12],
+ cameraMatrix[13],
+ cameraMatrix[14],
+ ];
+
+ var up = [0, 1, 0];
+
+ // Вычисляем матрицу камеры, используя look at.
+ var cameraMatrix = m4.lookAt(cameraPosition, fPosition, up);
+
+ // Делаем матрицу вида из матрицы камеры.
+ var viewMatrix = m4.inverse(cameraMatrix);
+
+ ...
+```
+
+И вот результат.
+
+{{{example url="../webgl-3d-camera-look-at.html" }}}
+
+Перетащите слайдер и обратите внимание, как камера отслеживает одну 'F'.
+
+Обратите внимание, что вы можете использовать математику "lookAt" не только для камер. Общие применения - заставить голову персонажа
+следовать за кем-то. Заставить башню целиться в цель. Заставить объект следовать по пути. Вы вычисляете,
+где на пути находится цель. Затем вы вычисляете, где на пути цель будет через несколько мгновений
+в будущем. Подставьте эти 2 значения в вашу функцию `lookAt`, и вы получите матрицу, которая заставляет
+ваш объект следовать по пути и ориентироваться к пути тоже.
+
+Прежде чем вы продолжите, вы можете захотеть проверить [эту короткую заметку о названиях матриц](webgl-matrix-naming.html).
+
+Иначе давайте [изучим анимацию дальше](webgl-animation.html).
+
+
+
стандарты lookAt
+
Большинство 3D математических библиотек имеют функцию lookAt. Часто она разработана
+специально для создания "матрицы вида", а не "матрицы камеры". Другими словами,
+она создает матрицу, которая перемещает все остальное перед камерой, а не
+матрицу, которая перемещает саму камеру.
+
Я нахожу это менее полезным. Как указано, функция lookAt имеет много применений. Легко
+вызвать inverse, когда вам нужна матрица вида, но если вы используете lookAt
+для того, чтобы заставить голову какого-то персонажа следовать за другим персонажем или какую-то башню целиться
+в свою цель, гораздо более полезно, если lookAt возвращает матрицу, которая ориентирует
+и позиционирует объект в мировом пространстве, по моему мнению.
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-geometry-lathe.md b/webgl/lessons/ru/webgl-3d-geometry-lathe.md
new file mode 100644
index 000000000..c67315144
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-geometry-lathe.md
@@ -0,0 +1,880 @@
+Title: WebGL2 3D Геометрия - Токарная обработка
+Description: Как создать токарную поверхность из кривой Безье.
+TOC: 3D Геометрия - Токарная обработка
+
+
+Это, вероятно, довольно специфическая тема, но я нашел её интересной, поэтому пишу об этом.
+Это не то, что я рекомендую вам делать на практике. Скорее, я думаю, что работа над
+этой темой поможет проиллюстрировать некоторые аспекты создания 3D моделей для WebGL.
+
+Кто-то спросил, как создать форму кегли для боулинга в WebGL. *Умный* ответ:
+"Используйте 3D пакет моделирования, такой как [Blender](https://blender.org),
+[Maya](https://www.autodesk.com/products/maya/overview),
+[3D Studio Max](https://www.autodesk.com/products/3ds-max/overview),
+[Cinema 4D](https://www.maxon.net/en/products/cinema-4d/overview/), и т.д.
+Используйте его для моделирования кегли, экспортируйте, прочитайте данные.
+([Формат OBJ относительно прост](https://en.wikipedia.org/wiki/Wavefront_.obj_file)).
+
+Но это заставило меня задуматься, а что если они хотели создать пакет моделирования?
+
+Есть несколько идей. Одна из них - создать цилиндр и попытаться сжать его в
+нужных местах, используя синусоидальные волны, примененные в определенных местах. Проблема
+с этой идеей в том, что вы не получите гладкую вершину. Стандартный цилиндр
+генерируется как серия равноудаленных колец, но вам понадобится больше
+колец там, где вещи более изогнуты.
+
+В пакете моделирования вы бы создали кеглю, сделав 2D силуэт или, скорее,
+изогнутую линию, которая соответствует краю 2D силуэта. Затем вы бы
+выточили это в 3D форму. Под *токарной обработкой* я имею в виду, что вы бы вращали
+это вокруг некоторой оси и генерировали бы точки в процессе. Это позволяет легко создавать
+любые круглые объекты, такие как чаша, стакан, бейсбольная бита, бутылки,
+лампочки и т.д.
+
+Итак, как мы это делаем? Ну, сначала нам нужен способ создать кривую.
+Затем нам нужно будет вычислить точки на этой кривой. Мы бы затем вращали
+эти точки вокруг некоторой оси, используя [матричную математику](webgl-2d-matrices.html),
+и строили треугольники из этих точек.
+
+Самый распространенный вид кривой в компьютерной графике, кажется,
+кривая Безье. Если вы когда-либо редактировали кривую в
+[Adobe Illustrator](https://www.adobe.com/products/illustrator.html) или
+[Inkscape](https://inkscape.org/en/) или
+[Affinity Designer](https://affinity.serif.com/en-us/designer/)
+или подобных программах, это кривая Безье.
+
+Кривая Безье, или скорее кубическая кривая Безье, формируется 4 точками.
+2 точки - это конечные точки. 2 точки - это "контрольные точки".
+
+Вот 4 точки
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=0" }}}
+
+Мы выбираем число между 0 и 1 (называемое `t`), где 0 = начало
+и 1 = конец. Затем мы вычисляем соответствующую точку `t`
+между каждой парой точек. `P1 P2`, `P2 P3`, `P3 P4`.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=1" }}}
+
+Другими словами, если `t = .25`, то мы вычисляем точку на 25% пути
+от `P1` к `P2`, еще одну на 25% пути от `P2` к `P3`
+и еще одну на 25% пути от `P3` к `P4`.
+
+Вы можете перетащить ползунок, чтобы настроить `t`, и вы также можете перемещать точки
+`P1`, `P2`, `P3` и `P4`.
+
+Мы делаем то же самое для результирующих точек. Вычисляем точки `t` между `Q1 Q2`
+и `Q2 Q3`.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=2" }}}
+
+Наконец, мы делаем то же самое для этих 2 точек и вычисляем точку `t` между
+`R1 R2`.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=3" }}}
+
+Позиции этой красной точки образуют кривую.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=4" }}}
+
+Итак, это кубическая кривая Безье.
+
+Обратите внимание, что хотя интерполяция между точками выше и
+процесс создания 3 точек из 4, затем 2 из 3, и наконец 1
+точки из 2 работает, это не обычный способ. Вместо этого кто-то подставил
+всю математику и упростил её до формулы, подобной этой
+
+
+
+Где `P1`, `P2`, `P3`, `P4` - это точки, как в примерах выше, а `P`
+- это красная точка.
+
+В 2D программе векторной графики, такой как Adobe Illustrator,
+когда вы создаете более длинную кривую, она фактически состоит из множества маленьких 4-точечных
+кривых, подобных этой. По умолчанию большинство приложений блокируют контрольные точки
+вокруг общей начальной/конечной точки и обеспечивают, чтобы они всегда были
+противоположны относительно общей точки.
+
+Смотрите этот пример, переместите `P3` или `P5`, и код переместит другой.
+
+{{{diagram url="resources/bezier-curve-edit.html" }}}
+
+Обратите внимание, что кривая, созданная `P1,P2,P3,P4`, является отдельной кривой от
+той, что создана `P4,P5,P6,P7`. Просто когда `P3` и `P5` находятся на точных
+противоположных сторонах `P4`, вместе они выглядят как одна непрерывная кривая.
+Большинство приложений обычно дают вам возможность прекратить блокировку их
+вместе, чтобы вы могли получить острый угол. Снимите флажок блокировки,
+затем перетащите `P3` или `P5`, и станет еще более ясно, что они являются
+отдельными кривыми.
+
+Далее нам нужен способ генерировать точки на кривой.
+Используя формулу выше, мы можем сгенерировать точку для
+заданного значения `t`, как это.
+
+ function getPointOnBezierCurve(points, offset, t) {
+ const invT = (1 - t);
+ return v2.add(v2.mult(points[offset + 0], invT * invT * invT),
+ v2.mult(points[offset + 1], 3 * t * invT * invT),
+ v2.mult(points[offset + 2], 3 * invT * t * t),
+ v2.mult(points[offset + 3], t * t *t));
+ }
+
+И мы можем сгенерировать набор точек для кривой, как это
+
+ function getPointsOnBezierCurve(points, offset, numPoints) {
+ const cpoints = [];
+ for (let i = 0; i < numPoints; ++i) {
+ const t = i / (numPoints - 1);
+ cpoints.push(getPointOnBezierCurve(points, offset, t));
+ }
+ return cpoints;
+ }
+
+Примечание: `v2.mult` и `v2.add` - это маленькие JavaScript функции, которые я включил
+для помощи в математических операциях с точками.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=0&showCurve=true&showPoints=true" }}}
+
+В диаграмме выше вы можете выбрать количество точек. Если кривая острая,
+вам понадобится больше точек. Если кривая почти прямая линия, то
+вам, вероятно, понадобится меньше точек. Одно решение
+- проверить, насколько изогнута кривая. Если она слишком изогнута, то разделить её на
+2 кривые.
+
+Часть разделения оказывается легкой. Если мы посмотрим на различные
+уровни интерполяции снова, точки `P1`, `Q1`, `R1`, КРАСНАЯ образуют одну
+кривую, а точки КРАСНАЯ, `R2`, `Q3`, `P4` образуют другую для любого значения t.
+Другими словами, мы можем разделить кривую где угодно и получить 2 кривые,
+которые соответствуют оригиналу.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=4&show2Curves=true" }}}
+
+Вторая часть - решить, нужно ли разделять кривую или нет. Просматривая
+интернет, я нашел [эту функцию](https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/),
+которая для данной кривой решает, насколько она плоская.
+
+ function flatness(points, offset) {
+ const p1 = points[offset + 0];
+ const p2 = points[offset + 1];
+ const p3 = points[offset + 2];
+ const p4 = points[offset + 3];
+
+ let ux = 3 * p2[0] - 2 * p1[0] - p4[0]; ux *= ux;
+ let uy = 3 * p2[1] - 2 * p1[1] - p4[1]; uy *= uy;
+ let vx = 3 * p3[0] - 2 * p4[0] - p1[0]; vx *= vx;
+ let vy = 3 * p3[1] - 2 * p4[1] - p1[1]; vy *= vy;
+
+ if(ux < vx) {
+ ux = vx;
+ }
+
+ if(uy < vy) {
+ uy = vy;
+ }
+
+ return ux + uy;
+ }
+
+Мы можем использовать это в нашей функции, которая получает точки для кривой.
+Сначала мы проверим, не слишком ли изогнута кривая. Если да, то разделим,
+если нет, то добавим точки.
+
+ function getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints) {
+ const outPoints = newPoints || [];
+ if (flatness(points, offset) < tolerance) {
+
+ // просто добавляем конечные точки этой кривой
+ outPoints.push(points[offset + 0]);
+ outPoints.push(points[offset + 3]);
+
+ } else {
+
+ // разделяем
+ const t = .5;
+ const p1 = points[offset + 0];
+ const p2 = points[offset + 1];
+ const p3 = points[offset + 2];
+ const p4 = points[offset + 3];
+
+ const q1 = v2.lerp(p1, p2, t);
+ const q2 = v2.lerp(p2, p3, t);
+ const q3 = v2.lerp(p3, p4, t);
+
+ const r1 = v2.lerp(q1, q2, t);
+ const r2 = v2.lerp(q2, q3, t);
+
+ const red = v2.lerp(r1, r2, t);
+
+ // делаем первую половину
+ getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
+ // делаем вторую половину
+ getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);
+
+ }
+ return outPoints;
+ }
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=0&showCurve=true&showTolerance=true" }}}
+
+Этот алгоритм хорошо справляется с обеспечением достаточного количества точек, но
+он не так хорошо справляется с удалением ненужных точек.
+
+Для этого мы обращаемся к [алгоритму Рамера-Дугласа-Пекера](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm),
+который я нашел в интернете.
+
+В этом алгоритме мы берем список точек.
+Мы находим самую дальнюю точку от линии, образованной 2 конечными точками.
+Затем мы проверяем, находится ли эта точка дальше от линии, чем некоторое расстояние.
+Если она меньше этого расстояния, мы просто оставляем 2 конечные точки и отбрасываем остальные.
+В противном случае мы запускаем алгоритм снова, один раз с точками от начала до самой дальней
+точки и снова от самой дальней точки до конечной точки.
+
+ function simplifyPoints(points, start, end, epsilon, newPoints) {
+ const outPoints = newPoints || [];
+
+ // находим самую дальнюю точку от конечных точек
+ const s = points[start];
+ const e = points[end - 1];
+ let maxDistSq = 0;
+ let maxNdx = 1;
+ for (let i = start + 1; i < end - 1; ++i) {
+ const distSq = v2.distanceToSegmentSq(points[i], s, e);
+ if (distSq > maxDistSq) {
+ maxDistSq = distSq;
+ maxNdx = i;
+ }
+ }
+
+ // если эта точка слишком далеко
+ if (Math.sqrt(maxDistSq) > epsilon) {
+
+ // разделяем
+ simplifyPoints(points, start, maxNdx + 1, epsilon, outPoints);
+ simplifyPoints(points, maxNdx, end, epsilon, outPoints);
+
+ } else {
+
+ // добавляем 2 конечные точки
+ outPoints.push(s, e);
+ }
+
+ return outPoints;
+ }
+
+`v2.distanceToSegmentSq` - это функция, которая вычисляет квадрат расстояния от точки
+до отрезка линии. Мы используем квадрат расстояния, потому что его быстрее вычислять, чем
+фактическое расстояние. Поскольку нас интересует только то, какая точка самая дальняя, квадрат расстояния
+будет работать так же хорошо, как и фактическое расстояние.
+
+Вот это в действии. Настройте расстояние, чтобы увидеть больше точек добавленных или удаленных.
+
+{{{diagram url="resources/bezier-curve-diagram.html?maxDepth=0&showCurve=true&showDistance=true" }}}
+
+Вернемся к нашей кегле. Мы могли бы попытаться расширить код выше в полноценный редактор.
+Ему нужно было бы уметь добавлять и удалять точки, блокировать и разблокировать контрольные точки.
+Ему понадобился бы откат и т.д... Но есть более простой способ. Мы можем просто использовать любой из
+основных редакторов, упомянутых выше. [Я использовал этот онлайн редактор](https://svg-edit.github.io/svgedit/).
+
+Вот SVG силуэт кегли, который я сделал.
+
+
+
+Он сделан из 4 кривых Безье. Данные для этого пути выглядят так
+
+
+
+[Интерпретируя эти данные](https://developer.mozilla.org/en/docs/Web/SVG/Tutorial/Paths), мы получаем эти точки.
+
+ ___
+ 44, 371, |
+ 62, 338, | 1-я кривая
+ 63, 305,___|__
+ 59, 260,___| |
+ 55, 215, | 2-я кривая
+ 22, 156,______|__
+ 20, 128,______| |
+ 18, 100, | 3-я кривая
+ 31, 77,_________|__
+ 36, 47,_________| |
+ 41, 17, | 4-я кривая
+ 39, -16, |
+ 0, -16,____________|
+
+Теперь, когда у нас есть данные для кривых, нам нужно вычислить некоторые точки
+на них.
+
+ // получает точки по всем сегментам
+ function getPointsOnBezierCurves(points, tolerance) {
+ const newPoints = [];
+ const numSegments = (points.length - 1) / 3;
+ for (let i = 0; i < numSegments; ++i) {
+ const offset = i * 3;
+ getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
+ }
+ return newPoints;
+ }
+
+Мы бы вызвали `simplifyPoints` для результата.
+
+Теперь нам нужно вращать их. Мы решаем, сколько делений сделать, для каждого деления
+вы используете [матричную математику](webgl-2d-matrices.html) для вращения точек вокруг оси Y.
+Как только мы создали все точки, мы соединяем их треугольниками, используя индексы.
+
+ // вращает вокруг оси Y.
+ function lathePoints(points,
+ startAngle, // угол для начала (т.е. 0)
+ endAngle, // угол для окончания (т.е. Math.PI * 2)
+ numDivisions, // сколько четырехугольников сделать вокруг
+ capStart, // true для закрытия начала
+ capEnd) { // true для закрытия конца
+ const positions = [];
+ const texcoords = [];
+ const indices = [];
+
+ const vOffset = capStart ? 1 : 0;
+ const pointsPerColumn = points.length + vOffset + (capEnd ? 1 : 0);
+ const quadsDown = pointsPerColumn - 1;
+
+ // генерируем точки
+ for (let division = 0; division <= numDivisions; ++division) {
+ const u = division / numDivisions;
+ const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
+ const mat = m4.yRotation(angle);
+ if (capStart) {
+ // добавляем точку на оси Y в начале
+ positions.push(0, points[0][1], 0);
+ texcoords.push(u, 0);
+ }
+ points.forEach((p, ndx) => {
+ const tp = m4.transformPoint(mat, [...p, 0]);
+ positions.push(tp[0], tp[1], tp[2]);
+ const v = (ndx + vOffset) / quadsDown;
+ texcoords.push(u, v);
+ });
+ if (capEnd) {
+ // добавляем точку на оси Y в конце
+ positions.push(0, points[points.length - 1][1], 0);
+ texcoords.push(u, 1);
+ }
+ }
+
+ // генерируем индексы
+ for (let division = 0; division < numDivisions; ++division) {
+ const column1Offset = division * pointsPerColumn;
+ const column2Offset = column1Offset + pointsPerColumn;
+ for (let quad = 0; quad < quadsDown; ++quad) {
+ indices.push(column1Offset + quad, column2Offset + quad, column1Offset + quad + 1);
+ indices.push(column1Offset + quad + 1, column2Offset + quad, column2Offset + quad + 1);
+ }
+ }
+
+ return {
+ position: positions,
+ texcoord: texcoords,
+ indices: indices,
+ };
+ }
+
+Код выше генерирует позиции и текстурные координаты, затем генерирует индексы для создания треугольников
+из них. `capStart` и `capEnd` указывают, генерировать ли точки закрытия. Представьте,
+что мы делаем банку. Эти опции указывали бы, закрывать ли концы.
+
+Используя наш [упрощенный код](webgl-less-code-more-fun.html), мы можем генерировать WebGL буферы с
+этими данными, как это
+
+ const tolerance = 0.15;
+ const distance = .4;
+ const divisions = 16;
+ const startAngle = 0;
+ const endAngle = Math.PI * 2;
+ const capStart = true;
+ const capEnd = true;
+
+ const tempPoints = getPointsOnBezierCurves(curvePoints, tolerance);
+ const points = simplifyPoints(tempPoints, 0, tempPoints.length, distance);
+ const arrays = lathePoints(points, startAngle, endAngle, divisions, capStart, capEnd);
+ const extents = getExtents(arrays.position);
+ if (!bufferInfo) {
+ bufferInfo = webglUtils.createBufferInfoFromArrays(gl, arrays);
+
+Вот пример
+
+{{{example url="../webgl-3d-lathe-step-01.html" }}}
+
+Поиграйте с ползунками, чтобы увидеть, как они влияют на результат.
+
+Однако есть проблема. Включите треугольники, и вы увидите, что текстура не применяется равномерно.
+Это потому, что мы основали координату `v` на индексе точек на линии. Если бы они были равномерно распределены,
+это могло бы работать. Но они не равномерно распределены, поэтому нам нужно сделать что-то другое.
+
+Мы можем пройти по точкам и вычислить общую длину кривой и расстояние каждой точки
+на этой кривой. Затем мы можем разделить на длину и получить лучшее значение
+для `v`.
+
+ // вращает вокруг оси Y.
+ function lathePoints(points,
+ startAngle, // угол для начала (т.е. 0)
+ endAngle, // угол для окончания (т.е. Math.PI * 2)
+ numDivisions, // сколько четырехугольников сделать вокруг
+ capStart, // true для закрытия верха
+ capEnd) { // true для закрытия низа
+ const positions = [];
+ const texcoords = [];
+ const indices = [];
+
+ const vOffset = capStart ? 1 : 0;
+ const pointsPerColumn = points.length + vOffset + (capEnd ? 1 : 0);
+ const quadsDown = pointsPerColumn - 1;
+
+ + // генерируем v координаты
+ + let vcoords = [];
+ +
+ + // сначала вычисляем длину точек
+ + let length = 0;
+ + for (let i = 0; i < points.length - 1; ++i) {
+ + vcoords.push(length);
+ + length += v2.distance(points[i], points[i + 1]);
+ + }
+ + vcoords.push(length); // последняя точка
+ +
+ + // теперь делим каждую на общую длину;
+ + vcoords = vcoords.map(v => v / length);
+
+ // генерируем точки
+ for (let division = 0; division <= numDivisions; ++division) {
+ const u = division / numDivisions;
+ const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
+ const mat = m4.yRotation(angle);
+ if (capStart) {
+ // добавляем точку на оси Y в начале
+ positions.push(0, points[0][1], 0);
+ texcoords.push(u, 0);
+ }
+ points.forEach((p, ndx) => {
+ const tp = m4.transformPoint(mat, [...p, 0]);
+ positions.push(tp[0], tp[1], tp[2]);
+ * texcoords.push(u, vcoords[ndx]);
+ });
+ if (capEnd) {
+ // добавляем точку на оси Y в конце
+ positions.push(0, points[points.length - 1][1], 0);
+ texcoords.push(u, 1);
+ }
+ }
+
+ // генерируем индексы
+ for (let division = 0; division < numDivisions; ++division) {
+ const column1Offset = division * pointsPerColumn;
+ const column2Offset = column1Offset + pointsPerColumn;
+ for (let quad = 0; quad < quadsDown; ++quad) {
+ indices.push(column1Offset + quad, column1Offset + quad + 1, column2Offset + quad);
+ indices.push(column1Offset + quad + 1, column2Offset + quad + 1, column2Offset + quad);
+ }
+ }
+
+ return {
+ position: positions,
+ texcoord: texcoords,
+ indices: indices,
+ };
+ }
+
+И вот результат
+
+{{{example url="../webgl-3d-lathe-step-02.html" }}}
+
+Эти координаты текстуры все еще не идеальны. Мы не решили, что делать для крышек.
+Это еще одна причина, почему вы должны просто использовать программу моделирования. Мы могли бы придумать
+разные идеи о том, как вычислять uv координаты для крышек, но они, вероятно, не будут
+особенно полезными. Если вы [погуглите "UV map a barrel"](https://www.google.com/search?q=uv+map+a+barrel),
+вы увидите, что получение идеальных UV координат - это не столько математическая проблема, сколько проблема ввода данных,
+и вам нужны хорошие инструменты для ввода этих данных.
+
+Есть еще одна вещь, которую мы должны сделать, и это добавить нормали.
+
+Мы могли бы вычислить нормаль для каждой точки на кривой. Фактически, если вы вернетесь к примерам
+на этой странице, вы можете увидеть, что линия, образованная `R1` и `R2`, является касательной к кривой.
+
+
+
+Нормаль перпендикулярна касательной, поэтому было бы легко использовать касательные
+для генерации нормалей.
+
+Но давайте представим, что мы хотели сделать подсвечник с силуэтом, как этот
+
+
+
+Есть много гладких областей, но также много острых углов. Как мы решаем, какие нормали
+использовать? Хуже того, когда мы хотим острый край, нам нужны дополнительные вершины. Потому что вершины
+имеют как позицию, так и нормаль, если нам нужна другая нормаль для чего-то в той же
+позиции, то нам нужна другая вершина. Вот почему, если мы делаем куб,
+нам фактически нужно как минимум 24 вершины. Хотя у куба только 8 углов, каждой
+грани куба нужны разные нормали в этих углах.
+
+При генерации куба легко просто генерировать правильные нормали, но для
+более сложной формы нет простого способа решить.
+
+Все программы моделирования имеют различные опции для генерации нормалей. Обычный способ - для каждой
+отдельной вершины они усредняют нормали всех многоугольников, которые используют эту вершину. За исключением того, что они
+позволяют пользователю выбрать некоторый максимальный угол. Если угол между одним многоугольником, используемым
+вершиной, больше этого максимального угла, то они генерируют новую вершину.
+
+Давайте сделаем это.
+
+ function generateNormals(arrays, maxAngle) {
+ const positions = arrays.position;
+ const texcoords = arrays.texcoord;
+
+ // сначала вычисляем нормаль каждого лица
+ let getNextIndex = makeIndiceIterator(arrays);
+ const numFaceVerts = getNextIndex.numElements;
+ const numVerts = arrays.position.length;
+ const numFaces = numFaceVerts / 3;
+ const faceNormals = [];
+
+ // Вычисляем нормаль для каждого лица.
+ // Делая это, создаем новую вершину для каждой вершины лица
+ for (let i = 0; i < numFaces; ++i) {
+ const n1 = getNextIndex() * 3;
+ const n2 = getNextIndex() * 3;
+ const n3 = getNextIndex() * 3;
+
+ const v1 = positions.slice(n1, n1 + 3);
+ const v2 = positions.slice(n2, n2 + 3);
+ const v3 = positions.slice(n3, n3 + 3);
+
+ faceNormals.push(m4.normalize(m4.cross(m4.subtractVectors(v1, v2), m4.subtractVectors(v3, v2))));
+ }
+
+ let tempVerts = {};
+ let tempVertNdx = 0;
+
+ // это предполагает, что позиции вершин точно совпадают
+
+ function getVertIndex(x, y, z) {
+
+ const vertId = x + "," + y + "," + z;
+ const ndx = tempVerts[vertId];
+ if (ndx !== undefined) {
+ return ndx;
+ }
+ const newNdx = tempVertNdx++;
+ tempVerts[vertId] = newNdx;
+ return newNdx;
+ }
+
+ // Нам нужно выяснить общие вершины.
+ // Это не так просто, как смотреть на лица (треугольники),
+ // потому что, например, если у нас есть стандартный цилиндр
+ //
+ //
+ // 3-4
+ // / \
+ // 2 5 Смотрим вниз на цилиндр, начиная с S
+ // | | и идя вокруг к E, E и S не являются
+ // 1 6 той же вершиной в данных, которые у нас есть,
+ // \ / поскольку они не используют общие UV координаты.
+ // S/E
+ //
+ // вершины в начале и конце не используют общие вершины,
+ // поскольку у них разные UV, но если вы не считаете
+ // их общими вершинами, они получат неправильные нормали
+
+ const vertIndices = [];
+ for (let i = 0; i < numVerts; ++i) {
+ const offset = i * 3;
+ const vert = positions.slice(offset, offset + 3);
+ vertIndices.push(getVertIndex(vert));
+ }
+
+ // проходим через каждую вершину и записываем, на каких лицах она находится
+ const vertFaces = [];
+ getNextIndex.reset();
+ for (let i = 0; i < numFaces; ++i) {
+ for (let j = 0; j < 3; ++j) {
+ const ndx = getNextIndex();
+ const sharedNdx = vertIndices[ndx];
+ let faces = vertFaces[sharedNdx];
+ if (!faces) {
+ faces = [];
+ vertFaces[sharedNdx] = faces;
+ }
+ faces.push(i);
+ }
+ }
+
+ // теперь проходим через каждое лицо и вычисляем нормали для каждой
+ // вершины лица. Включаем только лица, которые не отличаются больше чем
+ // на maxAngle. Добавляем результат в массивы newPositions,
+ // newTexcoords и newNormals, отбрасывая любые вершины, которые
+ // одинаковы.
+ tempVerts = {};
+ tempVertNdx = 0;
+ const newPositions = [];
+ const newTexcoords = [];
+ const newNormals = [];
+
+ function getNewVertIndex(x, y, z, nx, ny, nz, u, v) {
+ const vertId =
+ x + "," + y + "," + z + "," +
+ nx + "," + ny + "," + nz + "," +
+ u + "," + v;
+
+ const ndx = tempVerts[vertId];
+ if (ndx !== undefined) {
+ return ndx;
+ }
+ const newNdx = tempVertNdx++;
+ tempVerts[vertId] = newNdx;
+ newPositions.push(x, y, z);
+ newNormals.push(nx, ny, nz);
+ newTexcoords.push(u, v);
+ return newNdx;
+ }
+
+ const newVertIndices = [];
+
+ getNextIndex.reset();
+ const maxAngleCos = Math.cos(maxAngle);
+ // для каждого лица
+ for (let i = 0; i < numFaces; ++i) {
+ // получаем нормаль для этого лица
+ const thisFaceNormal = faceNormals[i];
+ // для каждой вершины на лице
+ for (let j = 0; j < 3; ++j) {
+ const ndx = getNextIndex();
+ const sharedNdx = vertIndices[ndx];
+ const faces = vertFaces[sharedNdx];
+ const norm = [0, 0, 0];
+ faces.forEach(faceNdx => {
+ // это лицо смотрит в том же направлении
+ const otherFaceNormal = faceNormals[faceNdx];
+ const dot = m4.dot(thisFaceNormal, otherFaceNormal);
+ if (dot > maxAngleCos) {
+ m4.addVectors(norm, otherFaceNormal, norm);
+ }
+ });
+ m4.normalize(norm, norm);
+ const poffset = ndx * 3;
+ const toffset = ndx * 2;
+ newVertIndices.push(getNewVertIndex(
+ positions[poffset + 0], positions[poffset + 1], positions[poffset + 2],
+ norm[0], norm[1], norm[2],
+ texcoords[toffset + 0], texcoords[toffset + 1]));
+ }
+ }
+
+ return {
+ position: newPositions,
+ texcoord: newTexcoords,
+ normal: newNormals,
+ indices: newVertIndices,
+ };
+
+ }
+
+ function makeIndexedIndicesFn(arrays) {
+ const indices = arrays.indices;
+ let ndx = 0;
+ const fn = function() {
+ return indices[ndx++];
+ };
+ fn.reset = function() {
+ ndx = 0;
+ };
+ fn.numElements = indices.length;
+ return fn;
+ }
+
+ function makeUnindexedIndicesFn(arrays) {
+ let ndx = 0;
+ const fn = function() {
+ return ndx++;
+ };
+ fn.reset = function() {
+ ndx = 0;
+ }
+ fn.numElements = arrays.positions.length / 3;
+ return fn;
+ }
+
+ function makeIndiceIterator(arrays) {
+ return arrays.indices
+ ? makeIndexedIndicesFn(arrays)
+ : makeUnindexedIndicesFn(arrays);
+ }
+
+В коде выше сначала мы генерируем нормали для каждого лица (каждого треугольника) из исходных точек.
+Затем мы генерируем набор индексов вершин, чтобы найти точки, которые одинаковы. Это потому, что когда мы вращали
+точки, первая точка и последняя точка должны совпадать, но у них разные UV координаты,
+поэтому они не являются одной и той же точкой. Для вычисления нормалей вершин нам нужно, чтобы они считались одной и той же
+точкой.
+
+После того, как это сделано, для каждой вершины мы составляем список всех лиц, которые она использует.
+
+Наконец, мы усредняем нормали всех лиц, которые использует каждая вершина, исключая те, которые отличаются
+больше чем на `maxAngle`, и генерируем новый набор вершин.
+
+Вот результат
+
+{{{example url="../webgl-3d-lathe-step-03.html"}}}
+
+Обратите внимание, что мы получаем острые края там, где мы их хотим. Сделайте `maxAngle` больше, и вы увидите, как эти края
+сглаживаются, когда соседние лица начинают включаться в вычисления нормалей.
+Также попробуйте настроить `divisions` на что-то вроде 5 или 6, затем настройте `maxAngle`, пока края
+вокруг не станут жесткими, но части, которые вы хотите сгладить, остались сглаженными. Вы также можете установить `mode`
+на `lit`, чтобы увидеть, как объект будет выглядеть с освещением, причина, по которой нам нужны нормали.
+
+## Итак, чему мы научились?
+
+Мы научились, что если вы хотите создавать 3D данные, **ИСПОЛЬЗУЙТЕ ПАКЕТ 3D МОДЕЛИРОВАНИЯ!!!** 😝
+
+Чтобы сделать что-то действительно полезное, вам, вероятно, понадобится настоящий [UV редактор](https://www.google.com/search?q=uv+editor).
+Работа с крышками также является чем-то, с чем поможет 3D редактор. Вместо использования
+ограниченного набора опций при токарной обработке, вы бы использовали другие функции редактора
+для добавления крышек и генерации более простых UV для крышек. 3D редакторы также поддерживают [выдавливание граней](https://www.google.com/search?q=extruding+model)
+и [выдавливание вдоль пути](https://www.google.com/search?q=extruding+along+a+path), которые, если вы посмотрите,
+должно быть довольно очевидно, как они работают, основываясь на примере токарной обработки выше.
+
+## Ссылки
+
+Я хотел упомянуть, что не смог бы сделать это без [этой потрясающей страницы о кривых Безье](https://pomax.github.io/bezierinfo/).
+
+
+
Что делает здесь оператор модуло?
+
Если вы внимательно посмотрите на функцию lathePoints, вы увидите этот модуло
+при вычислении угла.
Когда мы вращаем точки полностью вокруг круга, мы действительно хотим, чтобы первая
+и последняя точки совпадали. Math.sin(0) и Math.sin(Math.PI * 2)
+должны совпадать, но математика с плавающей точкой на компьютере не идеальна, и хотя они достаточно близки
+в общем, они не являются на самом деле 100% равными.
+
Это важно, когда мы пытаемся вычислить нормали. Мы хотим знать все лица, которые использует вершина.
+Мы вычисляем это, сравнивая вершины. Если 2 вершины равны, мы предполагаем, что они являются
+одной и той же вершиной. К сожалению, поскольку Math.sin(0) и Math.sin(Math.PI * 2)
+не равны, они не будут считаться одной и той же вершиной. Это означает, что при вычислении нормалей
+они не будут учитывать все лица, и их нормали будут неправильными.
+
Вот результат, когда это происходит
+
+
Как вы можете видеть, есть шов, где вершины не считаются общими,
+потому что они не являются 100% совпадением
+
Моя первая мысль была, что я должен изменить мое решение так, чтобы когда я проверяю совпадающие
+вершины, я проверял, находятся ли они в пределах некоторого расстояния. Если да, то они одна и та же вершина.
+Что-то вроде этого.
+
+const epsilon = 0.0001;
+const tempVerts = [];
+function getVertIndex(position) {
+ if (tempVerts.length) {
+ // найти ближайшую существующую вершину
+ let closestNdx = 0;
+ let closestDistSq = v2.distanceSq(position, tempVerts[0]);
+ for (let i = 1; i < tempVerts.length; ++i) {
+ let distSq = v2.distanceSq(position, tempVerts[i]);
+ if (distSq < closestDistSq) {
+ closestDistSq = distSq;
+ closestNdx = i;
+ }
+ }
+ // была ли ближайшая вершина достаточно близко?
+ if (closestDistSq < epsilon) {
+ // да, поэтому просто возвращаем индекс этой вершины.
+ return closestNdx;
+ }
+ }
+ // нет совпадения, добавляем вершину как новую вершину и возвращаем её индекс.
+ tempVerts.push(position);
+ return tempVerts.length - 1;
+}
+
+
Это сработало! Это убрало шов. К сожалению, это заняло несколько секунд для выполнения и
+сделало интерфейс непригодным для использования. Это потому, что это решение O^2. Если вы сдвинете ползунки
+для наибольшего количества вершин (distance/divisions) в примере выше, вы можете сгенерировать ~114000 вершин.
+Для O^2 это до 12 миллиардов итераций, которые должны произойти.
+
+
Я искал в интернете простое решение. Я не нашел. Я думал о том, чтобы поместить все точки
+в [октодерево](https://en.wikipedia.org/wiki/Octree), чтобы сделать поиск совпадающих точек
+быстрее, но это кажется слишком много для этой статьи.
+
+
Именно тогда я понял, что если единственная проблема - конечные точки, возможно, я мог бы добавить модуло
+к математике, чтобы точки были на самом деле одинаковыми. Исходный код был таким
+
Из-за модуло angle, когда endAngle равен Math.PI * 2, становится 0
+и поэтому он такой же, как начало. Шов исчез. Проблема решена!
+
Тем не менее, даже с изменением, если вы установите distance на 0.001
+и divisions на 60, это занимает почти секунду на моей машине для пересчета сетки. Хотя
+могут быть способы оптимизировать это, я думаю, что суть в понимании, что генерация сложных
+сеток - это вообще медленная операция. Это всего лишь один пример того, почему 3D игра может работать на 60fps,
+но 3D пакет моделирования часто работает на очень медленных частотах кадров.
+
+
+
+
+
Не является ли матричная математика избыточной здесь?
+
Когда мы вытачиваем точки, есть этот код для вращения.
Преобразование произвольной 3D точки матрицей 4x4 требует 16 умножений, 12 сложений и 3 деления.
+Мы могли бы упростить, просто используя [математику вращения в стиле единичного круга](webgl-2d-rotation.html).
+
+
+const s = Math.sin(angle);
+const c = Math.cos(angle);
+...
+points.forEach((p, ndx) => {
+ const x = p[0];
+ const y = p[1];
+ const z = p[2];
+ const tp = [
+ x * c - z * s,
+ y,
+ x * s + z * c,
+ ];
+ ...
+
+
+Это только 4 умножения и 2 сложения и без вызова функции, что, вероятно, как минимум в 6 раз быстрее.
+
+
+Стоит ли эта оптимизация? Ну, для этого конкретного примера я не думаю, что мы делаем достаточно,
+чтобы это имело значение. Моя мысль была, что вы могли бы захотеть позволить пользователю решить, вокруг какой оси
+вращаться. Использование матрицы сделало бы это легким, чтобы позволить пользователю передать ось
+и использовать что-то вроде
+
+
+ const mat = m4.axisRotation(userSuppliedAxis, angle);
+
+
Какой способ лучше, действительно зависит от вас и ваших потребностей. Я думаю, что я бы выбрал гибкость сначала
+и только позже оптимизировал, если что-то было слишком медленным для того, что я делал.
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-lighting-directional.md b/webgl/lessons/ru/webgl-3d-lighting-directional.md
new file mode 100644
index 000000000..6d4a6d1cc
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-lighting-directional.md
@@ -0,0 +1,536 @@
+Title: WebGL2 3D - Направленное освещение
+Description: Как реализовать направленное освещение в WebGL
+TOC: Направленное освещение
+
+
+Эта статья является продолжением [WebGL 3D Камеры](webgl-3d-camera.html).
+Если вы не читали это, я предлагаю [начать там](webgl-3d-camera.html).
+
+Есть много способов реализовать освещение. Возможно, самый простой - это *направленное освещение*.
+
+Направленное освещение предполагает, что свет идет равномерно с одного направления. Солнце
+в ясный день часто считается направленным светом. Оно так далеко, что его лучи
+можно считать падающими на поверхность объекта все параллельно.
+
+Вычисление направленного освещения на самом деле довольно просто. Если мы знаем, в каком направлении
+движется свет, и мы знаем, в каком направлении обращена поверхность объекта,
+мы можем взять *скалярное произведение* 2 направлений, и оно даст нам косинус
+угла между 2 направлениями.
+
+Вот пример
+
+{{{diagram url="resources/dot-product.html" caption="перетащите точки"}}}
+
+Перетащите точки вокруг, если вы получите их точно противоположными друг другу, вы увидите, что скалярное произведение
+равно -1. Если они находятся в точно том же месте, скалярное произведение равно 1.
+
+Как это полезно? Ну, если мы знаем, в каком направлении обращена поверхность нашего 3d объекта,
+и мы знаем направление, в котором светит свет, то мы можем просто взять скалярное произведение
+их, и оно даст нам число 1, если свет направлен прямо на
+поверхность, и -1, если они направлены прямо противоположно.
+
+{{{diagram url="resources/directional-lighting.html" caption="поверните направление" width="500" height="400"}}}
+
+Мы можем умножить наш цвет на это значение скалярного произведения, и бум! Свет!
+
+Одна проблема, как мы знаем, в каком направлении обращены поверхности нашего 3d объекта.
+
+## Представляем нормали
+
+Я не имею представления, почему они называются *нормалями*, но по крайней мере в 3D графике нормаль
+- это слово для единичного вектора, который описывает направление, в котором обращена поверхность.
+
+Вот некоторые нормали для куба и сферы.
+
+{{{diagram url="resources/normals.html"}}}
+
+Линии, торчащие из объектов, представляют нормали для каждой вершины.
+
+Обратите внимание, что у куба есть 3 нормали в каждом углу. Это потому, что вам нужно
+3 разные нормали, чтобы представить способ, которым каждая грань куба, эм, .. обращена.
+
+Здесь нормали также окрашены в зависимости от их направления с
+положительным x, будучи красным, вверх, будучи
+зеленым, и положительным z, будучи
+синим.
+
+Итак, давайте добавим нормали к нашему `F` из [наших предыдущих примеров](webgl-3d-camera.html),
+чтобы мы могли его осветить. Поскольку `F` очень блочный, и его грани выровнены
+по оси x, y или z, это будет довольно легко. Вещи, которые обращены вперед,
+имеют нормаль `0, 0, 1`. Вещи, которые обращены назад, имеют `0, 0, -1`. Обращенные
+влево имеют `-1, 0, 0`, обращенные вправо имеют `1, 0, 0`. Вверх - это `0, 1, 0`, а вниз - `0, -1, 0`.
+
+```
+function setNormals(gl) {
+ var normals = new Float32Array([
+ // левая колонка спереди
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+
+ // верхняя перекладина спереди
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+
+ // средняя перекладина спереди
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+ 0, 0, 1,
+
+ // левая колонка сзади
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+
+ // верхняя перекладина сзади
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+
+ // средняя перекладина сзади
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+ 0, 0, -1,
+
+ // верх
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+
+ // правая сторона верхней перекладины
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+
+ // под верхней перекладиной
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+
+ // между верхней перекладиной и средней
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+
+ // верх средней перекладины
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+ 0, 1, 0,
+
+ // правая сторона средней перекладины
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+
+ // низ средней перекладины
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+
+ // правая сторона низа
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+ 1, 0, 0,
+
+ // низ
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+ 0, -1, 0,
+
+ // левая сторона
+ -1, 0, 0,
+ -1, 0, 0,
+ -1, 0, 0,
+ -1, 0, 0,
+ -1, 0, 0,
+ -1, 0, 0,
+ ]);
+ gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
+}
+
+и настроим их. Пока мы этим занимаемся, давайте уберем цвета вершин,
+чтобы было легче увидеть освещение.
+
+ // look up where the vertex data needs to go.
+ var positionLocation = gl.getAttribLocation(program, "a_position");
+ -var colorLocation = gl.getAttribLocation(program, "a_color");
+ +var normalLocation = gl.getAttribLocation(program, "a_normal");
+
+ ...
+
+ -// Create a buffer for colors.
+ -var buffer = gl.createBuffer();
+ -gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ -gl.enableVertexAttribArray(colorLocation);
+ -
+ -// We'll supply RGB as bytes.
+ -gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
+ -
+ -// Set Colors.
+ -setColors(gl);
+
+ // Create a buffer for normals.
+ var buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+ gl.enableVertexAttribArray(normalLocation);
+ gl.vertexAttribPointer(normalLocation, 3, gl.FLOAT, false, 0, 0);
+
+ // Set normals.
+ setNormals(gl);
+
+Теперь нам нужно заставить наши шейдеры использовать их
+
+Сначала вершинный шейдер, мы просто передаем нормали в
+фрагментный шейдер
+
+```
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec4 a_position;
+-in vec4 a_color;
++in vec3 a_normal;
+
+// Матрица для преобразования позиций
+uniform mat4 u_matrix;
+
+-// varying для передачи цвета в фрагментный шейдер
+-out vec4 v_color;
+
++// varying для передачи нормали в фрагментный шейдер
++out vec3 v_normal;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_matrix * a_position;
+
+- // Передаем цвет в фрагментный шейдер.
+- v_color = a_color;
+
++ // Передаем нормаль в фрагментный шейдер
++ v_normal = a_normal;
+}
+```
+
+И фрагментный шейдер, мы будем делать математику, используя скалярное произведение
+направления света и нормали
+
+```
+#version 300 es
+
+precision highp float;
+
+-// изменяемый цвет, переданный из вершинного шейдера
+-in vec4 v_color;
+
++// Переданный и изменяемый из вершинного шейдера.
++in vec3 v_normal;
++
++uniform vec3 u_reverseLightDirection;
++uniform vec4 u_color;
+
+// нам нужно объявить выход для фрагментного шейдера
+out vec4 outColor;
+
+void main() {
+- outColor = v_color;
++ // потому что v_normal - это varying, он интерполируется
++ // поэтому он не будет единичным вектором. Нормализация его
++ // сделает его снова единичным вектором
++ vec3 normal = normalize(v_normal);
++
++ // вычисляем свет, беря скалярное произведение
++ // нормали к обратному направлению света
++ float light = dot(normal, u_reverseLightDirection);
++
++ outColor = u_color;
++
++ // Давайте умножим только цветовую часть (не альфа)
++ // на свет
++ outColor.rgb *= light;
+}
+```
+
+Затем нам нужно найти местоположения `u_color` и `u_reverseLightDirection`.
+
+```
+ // lookup uniforms
+ var matrixLocation = gl.getUniformLocation(program, "u_matrix");
++ var colorLocation = gl.getUniformLocation(program, "u_color");
++ var reverseLightDirectionLocation =
++ gl.getUniformLocation(program, "u_reverseLightDirection");
+
+```
+
+и нам нужно их установить
+
+```
+ // Set the matrix.
+ gl.uniformMatrix4fv(matrixLocation, false, matrix);
+
++ // Set the color to use
++ gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
++
++ // set the light direction.
++ gl.uniform3fv(reverseLightDirectionLocation, normalize([0.5, 0.7, 1]));
+```
+
+`normalize`, который мы разбирали раньше, сделает любые значения, которые мы туда положим,
+единичным вектором. Конкретные значения в примере:
+`x = 0.5`, что положительный `x` означает, что свет справа, указывая влево.
+`y = 0.7`, что положительный `y` означает, что свет сверху, указывая вниз.
+`z = 1`, что положительный `z` означает, что свет спереди, указывая в сцену.
+относительные значения означают, что направление в основном указывает в сцену
+и указывает больше вниз, чем вправо.
+
+И вот это
+
+{{{example url="../webgl-3d-lighting-directional.html" }}}
+
+Если вы повернете F, вы можете заметить что-то. F вращается,
+но освещение не меняется. Когда F вращается, мы хотим, чтобы та часть,
+которая обращена к направлению света, была самой яркой.
+
+Чтобы исправить это, нам нужно переориентировать нормали, когда объект переориентируется.
+Как мы делали для позиций, мы можем умножить нормали на какую-то матрицу. Самая очевидная
+матрица была бы `world` матрица. Как есть сейчас, мы передаем только
+1 матрицу, называемую `u_matrix`. Давайте изменим это, чтобы передавать 2 матрицы. Одну, называемую
+`u_world`, которая будет мировой матрицей. Другую, называемую `u_worldViewProjection`,
+которая будет тем, что мы сейчас передаем как `u_matrix`
+
+```
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec4 a_position;
+in vec3 a_normal;
+
+*uniform mat4 u_worldViewProjection;
++uniform mat4 u_world;
+
+out vec3 v_normal;
+
+void main() {
+ // Умножаем позицию на матрицу.
+* gl_Position = u_worldViewProjection * a_position;
+
+* // ориентируем нормали и передаем в фрагментный шейдер
+* v_normal = mat3(u_world) * a_normal;
+}
+```
+
+Обратите внимание, что мы умножаем `a_normal` на `mat3(u_world)`. Это
+потому, что нормали - это направление, поэтому нас не волнует трансляция.
+Ориентационная часть матрицы находится только в верхней области 3x3
+матрицы.
+
+Теперь нам нужно найти эти uniform переменные
+
+```
+ // lookup uniforms
+- var matrixLocation = gl.getUniformLocation(program, "u_matrix");
+* var worldViewProjectionLocation =
+* gl.getUniformLocation(program, "u_matrix");
++ var worldLocation = gl.getUniformLocation(program, "u_world");
+```
+
+И нам нужно изменить код, который их обновляет
+
+```
+*var worldMatrix = m4.yRotation(fRotationRadians);
+*var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix,
+ worldMatrix);
+
+*// Set the matrices
+*gl.uniformMatrix4fv(
+* worldViewProjectionLocation, false,
+* worldViewProjectionMatrix);
+*gl.uniformMatrix4fv(worldLocation, false, worldMatrix);
+```
+
+и вот это
+
+{{{example url="../webgl-3d-lighting-directional-world.html" }}}
+
+Поверните F и обратите внимание, какая бы сторона ни была обращена к направлению света, она освещается.
+
+Есть одна проблема, которую я не знаю, как показать напрямую, поэтому я
+покажу это в диаграмме. Мы умножаем `normal` на
+матрицу `u_world`, чтобы переориентировать нормали.
+Что происходит, если мы масштабируем мировую матрицу?
+Оказывается, мы получаем неправильные нормали.
+
+{{{diagram url="resources/normals-scaled.html" caption="нажмите, чтобы переключить нормали" width="600" }}}
+
+Я никогда не беспокоился понять
+решение, но оказывается, вы можете получить обратную матрицу мира,
+транспонировать ее, что означает поменять местами столбцы и строки, и использовать это вместо
+и вы получите правильный ответ.
+
+В диаграмме выше фиолетовая сфера
+не масштабирована. Красная сфера слева
+масштабирована, и нормали умножаются на мировую матрицу. Вы
+можете видеть, что что-то не так. Синяя
+сфера справа использует матрицу обратной транспонированной мира.
+
+Нажмите на диаграмму, чтобы переключаться между разными представлениями. Вы должны
+заметить, когда масштаб экстремальный, очень легко увидеть, что нормали
+слева (world) **не** остаются перпендикулярными к поверхности сферы,
+тогда как те, что справа (worldInverseTranspose), остаются перпендикулярными
+к сфере. Последний режим делает их все затененными красным. Вы должны увидеть, что освещение
+на 2 внешних сферах очень разное в зависимости от того, какая матрица используется.
+Трудно сказать, какая правильная, поэтому это тонкая проблема, но
+основанная на других визуализациях, ясно, что использование worldInverseTranspose
+правильно.
+
+Чтобы реализовать это в нашем примере, давайте изменим код так. Сначала мы обновим
+шейдер. Технически мы могли бы просто обновить значение `u_world`,
+но лучше, если мы переименуем вещи, чтобы они назывались тем, чем они на самом деле являются,
+иначе это будет запутанно.
+
+```
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec4 a_position;
+in vec3 a_normal;
+
+uniform mat4 u_worldViewProjection;
+-uniform mat4 u_world
++uniform mat4 u_worldInverseTranspose;
+
+// varyings для передачи нормали и цвета в фрагментный шейдер
+out vec4 v_color;
+out vec3 v_normal;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_worldViewProjection * a_position;
+
+ // ориентируем нормали и передаем в фрагментный шейдер
+* v_normal = mat3(u_worldInverseTranspose) * a_normal;
+}
+```
+
+Затем нам нужно найти это
+
+```
+- var worldLocation = gl.getUniformLocation(program, "u_world");
++ var worldInverseTransposeLocation =
++ gl.getUniformLocation(program, "u_worldInverseTranspose");
+```
+
+И нам нужно вычислить и установить это
+
+```
+var worldMatrix = m4.yRotation(fRotationRadians);
+var worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);
++var worldInverseMatrix = m4.inverse(worldMatrix);
++var worldInverseTransposeMatrix = m4.transpose(worldInverseMatrix);
+
+// Set the matrices
+gl.uniformMatrix4fv(
+ worldViewProjectionLocation, false,
+ worldViewProjectionMatrix);
+-gl.uniformMatrix4fv(worldLocation, false, worldMatrix);
++gl.uniformMatrix4fv(
++ worldInverseTransposeLocation, false,
++ worldInverseTransposeMatrix);
+```
+
+и вот код для транспонирования матрицы
+
+```
+var m4 = {
+ transpose: function(m) {
+ return [
+ m[0], m[4], m[8], m[12],
+ m[1], m[5], m[9], m[13],
+ m[2], m[6], m[10], m[14],
+ m[3], m[7], m[11], m[15],
+ ];
+ },
+ ...
+```
+
+Поскольку эффект тонкий и поскольку мы ничего не масштабируем,
+нет заметной разницы, но по крайней мере теперь мы готовы.
+
+{{{example url="../webgl-3d-lighting-directional-worldinversetranspose.html" }}}
+
+Я надеюсь, что этот первый шаг в освещение был ясен. Далее [точечное освещение](webgl-3d-lighting-point.html).
+
+
Потому что мы установили w в 0 перед умножением, это
+привело бы к умножению трансляции из матрицы на 0, эффективно удаляя ее. Я думаю, что это
+более распространенный способ сделать это. Способ mat3 выглядел чище для меня, но
+я часто делал это и так тоже.
+
Еще одно решение было бы сделать u_worldInverseTransposemat3.
+Есть 2 причины не делать этого. Одна в том, что у нас могут быть
+другие потребности для полного u_worldInverseTranspose, поэтому передача всего
+mat4 означает, что мы можем использовать это для этих других потребностей.
+Другая в том, что все наши матричные функции в JavaScript
+делают матрицы 4x4. Создание целого другого набора для матриц 3x3
+или даже преобразование из 4x4 в 3x3 - это работа, которую мы бы предпочли
+не делать, если бы не было более убедительной причины.
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-lighting-normal-mapping.md b/webgl/lessons/ru/webgl-3d-lighting-normal-mapping.md
new file mode 100644
index 000000000..6fb40932f
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-lighting-normal-mapping.md
@@ -0,0 +1,10 @@
+Title: WebGL2 3D - Карты нормалей
+Description: Как реализовать карты нормалей
+TOC: Карты нормалей
+
+В разработке
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-lighting-point.md b/webgl/lessons/ru/webgl-3d-lighting-point.md
new file mode 100644
index 000000000..d690deaf4
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-lighting-point.md
@@ -0,0 +1,372 @@
+Title: WebGL2 3D - Точечное освещение
+Description: Как реализовать точечное освещение в WebGL
+TOC: Точечное освещение
+
+
+Эта статья является продолжением [WebGL 3D Направленное освещение](webgl-3d-lighting-directional.html).
+Если вы не читали это, я предлагаю [начать там](webgl-3d-lighting-directional.html).
+
+В последней статье мы рассмотрели направленное освещение, где свет идет
+универсально с одного направления. Мы установили это направление перед рендерингом.
+
+Что если вместо установки направления для света мы выберем точку в 3d пространстве для света
+и вычислим направление от любого места на поверхности нашей модели в нашем шейдере?
+Это дало бы нам точечный свет.
+
+{{{diagram url="resources/point-lighting.html" width="500" height="400" className="noborder" }}}
+
+Если вы повернете поверхность выше, вы увидите, как каждая точка на поверхности имеет другой
+вектор *поверхность к свету*. Получение скалярного произведения нормали поверхности и каждого отдельного
+вектора поверхности к свету дает нам другое значение в каждой точке поверхности.
+
+Итак, давайте сделаем это.
+
+Сначала нам нужна позиция света
+
+ uniform vec3 u_lightWorldPosition;
+
+И нам нужен способ вычислить мировую позицию поверхности. Для этого мы можем умножить
+наши позиции на мировую матрицу, так что ...
+
+ uniform mat4 u_world;
+
+ ...
+
+ // вычисляем мировую позицию поверхности
+ vec3 surfaceWorldPosition = (u_world * a_position).xyz;
+
+И мы можем вычислить вектор от поверхности к свету, который похож на
+направление, которое у нас было раньше, за исключением того, что на этот раз мы вычисляем его для каждой позиции на
+поверхности к точке.
+
+ v_surfaceToLight = u_lightPosition - surfaceWorldPosition;
+
+Вот все это в контексте
+
+ #version 300 es
+
+ in vec4 a_position;
+ in vec3 a_normal;
+
+ +uniform vec3 u_lightWorldPosition;
+
+ +uniform mat4 u_world;
+ uniform mat4 u_worldViewProjection;
+ uniform mat4 u_worldInverseTranspose;
+
+ out vec3 v_normal;
+ +out vec3 v_surfaceToLight;
+
+ void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_worldViewProjection * a_position;
+
+ // ориентируем нормали и передаем в фрагментный шейдер
+ v_normal = mat3(u_worldInverseTranspose) * a_normal;
+
+ + // вычисляем мировую позицию поверхности
+ + vec3 surfaceWorldPosition = (u_world * a_position).xyz;
+ +
+ + // вычисляем вектор поверхности к свету
+ + // и передаем его в фрагментный шейдер
+ + v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
+ }
+
+Теперь в фрагментном шейдере нам нужно нормализовать вектор поверхности к свету,
+поскольку это не единичный вектор. Обратите внимание, что мы могли бы нормализовать в вершинном шейдере,
+но поскольку это *varying*, он будет линейно интерполироваться между нашими позициями
+и поэтому не будет полным единичным вектором
+
+ #version 300 es
+ precision highp float;
+
+ // Переданный из вершинного шейдера.
+ in vec3 v_normal;
+ +in vec3 v_surfaceToLight;
+
+ -uniform vec3 u_reverseLightDirection;
+ uniform vec4 u_color;
+
+ // нам нужно объявить выход для фрагментного шейдера
+ out vec4 outColor;
+
+ void main() {
+ // потому что v_normal - это varying, он интерполируется
+ // поэтому он не будет единичным вектором. Нормализация его
+ // сделает его снова единичным вектором
+ vec3 normal = normalize(v_normal);
+
+ vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+
+ - float light = dot(normal, u_reverseLightDirection);
+ + float light = dot(normal, surfaceToLightDirection);
+
+ outColor = u_color;
+
+ // Давайте умножим только цветовую часть (не альфа)
+ // на свет
+ outColor.rgb *= light;
+ }
+
+
+Затем нам нужно найти местоположения `u_world` и `u_lightWorldPosition`
+
+```
+- var reverseLightDirectionLocation =
+- gl.getUniformLocation(program, "u_reverseLightDirection");
++ var lightWorldPositionLocation =
++ gl.getUniformLocation(program, "u_lightWorldPosition");
++ var worldLocation =
++ gl.getUniformLocation(program, "u_world");
+```
+
+и установить их
+
+```
+ // Set the matrices
++ gl.uniformMatrix4fv(
++ worldLocation, false,
++ worldMatrix);
+ gl.uniformMatrix4fv(
+ worldViewProjectionLocation, false,
+ worldViewProjectionMatrix);
+
+ ...
+
+- // set the light direction.
+- gl.uniform3fv(reverseLightDirectionLocation, normalize([0.5, 0.7, 1]));
++ // set the light position
++ gl.uniform3fv(lightWorldPositionLocation, [20, 30, 50]);
+```
+
+И вот это
+
+{{{example url="../webgl-3d-lighting-point.html" }}}
+
+Теперь, когда у нас есть точка, мы можем добавить что-то, называемое бликовым освещением.
+
+Если вы посмотрите на объект в реальном мире, если он хоть немного блестящий, то если он случайно
+отражает свет прямо на вас, это почти как зеркало
+
+
+
+Мы можем симулировать этот эффект, вычисляя, отражается ли свет в наши глаза. Снова *скалярное произведение*
+приходит на помощь.
+
+Что нам нужно проверить? Ну, давайте подумаем об этом. Свет отражается под тем же углом, под которым он попадает на поверхность,
+поэтому если направление от поверхности к свету - это точное отражение от поверхности к глазу,
+то это под идеальным углом для отражения
+
+{{{diagram url="resources/surface-reflection.html" width="500" height="400" className="noborder" }}}
+
+Если мы знаем направление от поверхности нашей модели к свету (что мы знаем, поскольку мы только что это сделали).
+И если мы знаем направление от поверхности к виду/глазу/камере, которое мы можем вычислить, то мы можем добавить
+эти 2 вектора и нормализовать их, чтобы получить `halfVector`, который является вектором, который находится на полпути между ними.
+Если halfVector и нормаль поверхности совпадают, то это идеальный угол для отражения света в
+вид/глаз/камеру. И как мы можем сказать, когда они совпадают? Возьмите *скалярное произведение*, как мы делали
+раньше. 1 = они совпадают, то же направление, 0 = они перпендикулярны, -1 = они противоположны.
+
+{{{diagram url="resources/specular-lighting.html" width="500" height="400" className="noborder" }}}
+
+Итак, первое, что нам нужно, это передать позицию вида/камеры/глаза, вычислить вектор поверхности к виду
+и передать его в фрагментный шейдер.
+
+ #version 300 es
+
+ in vec4 a_position;
+ in vec3 a_normal;
+
+ uniform vec3 u_lightWorldPosition;
+ +uniform vec3 u_viewWorldPosition;
+
+ uniform mat4 u_world;
+ uniform mat4 u_worldViewProjection;
+ uniform mat4 u_worldInverseTranspose;
+
+ varying vec3 v_normal;
+
+ out vec3 v_surfaceToLight;
+ +out vec3 v_surfaceToView;
+
+ void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_worldViewProjection * a_position;
+
+ // ориентируем нормали и передаем в фрагментный шейдер
+ v_normal = mat3(u_worldInverseTranspose) * a_normal;
+
+ // вычисляем мировую позицию поверхности
+ vec3 surfaceWorldPosition = (u_world * a_position).xyz;
+
+ // вычисляем вектор поверхности к свету
+ v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
+
+ + // вычисляем вектор поверхности к виду/камере
+ + // и передаем его в фрагментный шейдер
+ + v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
+ }
+
+Далее в фрагментном шейдере нам нужно вычислить `halfVector` между
+векторами поверхности к виду и поверхности к свету. Затем мы можем взять скалярное
+произведение `halfVector` и нормали, чтобы выяснить, отражается ли свет
+в вид.
+
+ // Переданный из вершинного шейдера.
+ in vec3 v_normal;
+ in vec3 v_surfaceToLight;
+ +in vec3 v_surfaceToView;
+
+ uniform vec4 u_color;
+
+ 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 light = dot(normal, surfaceToLightDirection);
+ + float specular = dot(normal, halfVector);
+
+ outColor = u_color;
+
+ // Давайте умножим только цветовую часть (не альфа)
+ // на свет
+ outColor.rgb *= light;
+
+ + // Просто добавляем блик
+ + outColor.rgb += specular;
+ }
+
+Наконец, нам нужно найти `u_viewWorldPosition` и установить его
+
+ var lightWorldPositionLocation =
+ gl.getUniformLocation(program, "u_lightWorldPosition");
+ +var viewWorldPositionLocation =
+ + gl.getUniformLocation(program, "u_viewWorldPosition");
+
+ ...
+
+ // Вычисляем матрицу камеры
+ var camera = [100, 150, 200];
+ var target = [0, 35, 0];
+ var up = [0, 1, 0];
+ var cameraMatrix = makeLookAt(camera, target, up);
+
+ ...
+
+ +// устанавливаем позицию камеры/вида
+ +gl.uniform3fv(viewWorldPositionLocation, camera);
+
+
+И вот это
+
+{{{example url="../webgl-3d-lighting-point-specular.html" }}}
+
+**БОЖЕ, ЭТО ЯРКО!**
+
+Мы можем исправить яркость, возведя результат скалярного произведения в степень. Это сожмет
+бликовое освещение от линейного затухания до экспоненциального затухания.
+
+{{{diagram url="resources/power-graph.html" width="300" height="300" className="noborder" }}}
+
+Чем ближе красная линия к верху графика, тем ярче будет наше бликовое добавление.
+Возведя в степень, это сжимает диапазон, где он становится ярким, вправо.
+
+Давайте назовем это `shininess` и добавим в наш шейдер.
+
+ uniform vec4 u_color;
+ +uniform float u_shininess;
+
+ ...
+
+ - float specular = dot(normal, halfVector);
+ + float specular = 0.0;
+ + if (light > 0.0) {
+ + specular = pow(dot(normal, halfVector), u_shininess);
+ + }
+
+Скалярное произведение может быть отрицательным. Возведение отрицательного числа в степень не определено в WebGL,
+что было бы плохо. Итак, если скалярное произведение может быть отрицательным, то мы просто оставляем specular на 0.0.
+
+Конечно, нам нужно найти местоположение и установить его
+
+ +var shininessLocation = gl.getUniformLocation(program, "u_shininess");
+
+ ...
+
+ // устанавливаем блеск
+ gl.uniform1f(shininessLocation, shininess);
+
+И вот это
+
+{{{example url="../webgl-3d-lighting-point-specular-power.html" }}}
+
+Последняя вещь, которую я хочу рассмотреть в этой статье, это цвета света.
+
+До этого момента мы использовали `light` для умножения цвета, который мы передаем для
+F. Мы могли бы предоставить цвет света, если бы хотели цветные огни
+
+ uniform vec4 u_color;
+ uniform float u_shininess;
+ +uniform vec3 u_lightColor;
+ +uniform vec3 u_specularColor;
+
+ ...
+
+ // Давайте умножим только цветовую часть (не альфа)
+ // на свет
+ * outColor.rgb *= light * u_lightColor;
+
+ // Просто добавляем блик
+ * outColor.rgb += specular * u_specularColor;
+ }
+
+и конечно
+
+ + var lightColorLocation =
+ + gl.getUniformLocation(program, "u_lightColor");
+ + var specularColorLocation =
+ + gl.getUniformLocation(program, "u_specularColor");
+
+и
+
+ + // устанавливаем цвет света
+ + gl.uniform3fv(lightColorLocation, normalize([1, 0.6, 0.6])); // красный свет
+ + // устанавливаем цвет блика
+ + gl.uniform3fv(specularColorLocation, normalize([1, 0.2, 0.2])); // красный свет
+
+{{{example url="../webgl-3d-lighting-point-color.html" }}}
+
+Далее [прожекторное освещение](webgl-3d-lighting-spot.html).
+
+
+
Почему pow(negative, power) не определено?
+
Что это означает?
+
pow(5, 2)
+
Ну, вы можете смотреть на это как
+
5 * 5 = 25
+
А что насчет
+
pow(5, 3)
+
Ну, вы можете смотреть на это как
+
5 * 5 * 5 = 125
+
Хорошо, а как насчет
+
pow(-5, 2)
+
Ну, это могло бы быть
+
-5 * -5 = 25
+
И
+
pow(-5, 3)
+
Ну, вы можете смотреть на это как
+
-5 * -5 * -5 = -125
+
Как вы знаете, умножение отрицательного на отрицательное дает положительное. Умножение на отрицательное
+снова делает это отрицательным.
+
Ну, тогда что это означает?
+
pow(-5, 2.5)
+
Как вы решаете, какой результат этого положительный или отрицательный? Это
+земля мнимых чисел.
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-lighting-spot.md b/webgl/lessons/ru/webgl-3d-lighting-spot.md
new file mode 100644
index 000000000..f520ac41e
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-lighting-spot.md
@@ -0,0 +1,356 @@
+Title: WebGL2 3D - Прожекторное освещение
+Description: Как реализовать прожекторное освещение в WebGL
+TOC: Прожекторное освещение
+
+
+Эта статья является продолжением [WebGL 3D Точечное освещение](webgl-3d-lighting-point.html). Если вы не читали это, я предлагаю [начать там](webgl-3d-lighting-point.html).
+
+В последней статье мы рассмотрели точечное освещение, где для каждой точки
+на поверхности нашего объекта мы вычисляем направление от света
+к этой точке на поверхности. Затем мы делаем то же самое, что делали для
+[направленного освещения](webgl-3d-lighting-directional.html), что
+мы взяли скалярное произведение нормали поверхности (направление, в котором обращена поверхность)
+и направления света. Это дало нам значение
+1, если два направления совпадали и поэтому должны быть полностью освещены. 0, если
+два направления были перпендикулярны, и -1, если они были противоположны.
+Мы использовали это значение напрямую для умножения цвета поверхности,
+что дало нам освещение.
+
+Прожекторное освещение - это только очень небольшое изменение. На самом деле, если вы думаете
+творчески о том, что мы сделали до сих пор, вы могли бы
+вывести собственное решение.
+
+Вы можете представить точечный свет как точку со светом, идущим во всех
+направлениях от этой точки.
+Чтобы сделать прожектор, все, что нам нужно сделать, это выбрать направление от
+этой точки, это направление нашего прожектора. Затем, для каждого
+направления, в котором идет свет, мы могли бы взять скалярное произведение
+этого направления с нашим выбранным направлением прожектора. Мы бы выбрали какой-то произвольный
+предел и решили, если мы в пределах этого предела, мы освещаем. Если мы не в пределах
+предела, мы не освещаем.
+
+{{{diagram url="resources/spot-lighting.html" width="500" height="400" className="noborder" }}}
+
+В диаграмме выше мы можем видеть свет с лучами, идущими во всех направлениях, и
+напечатанными на них их скалярными произведениями относительно направления.
+Затем у нас есть конкретное **направление**, которое является направлением прожектора.
+Мы выбираем предел (выше он в градусах). Из предела мы вычисляем *dot limit*, мы просто берем косинус предела. Если скалярное произведение нашего выбранного направления прожектора к
+направлению каждого луча света выше dot limit, то мы делаем освещение. Иначе нет освещения.
+
+Чтобы сказать это по-другому, скажем, предел составляет 20 градусов. Мы можем преобразовать
+это в радианы и от этого к значению от -1 до 1, взяв косинус. Давайте назовем это dot space.
+Другими словами, вот небольшая таблица для значений предела
+
+ пределы в
+ градусах | радианах | dot space
+ --------+---------+----------
+ 0 | 0.0 | 1.0
+ 22 | .38 | .93
+ 45 | .79 | .71
+ 67 | 1.17 | .39
+ 90 | 1.57 | 0.0
+ 180 | 3.14 | -1.0
+
+Затем мы можем просто проверить
+
+ dotFromDirection = dot(surfaceToLight, -lightDirection)
+ if (dotFromDirection >= limitInDotSpace) {
+ // делаем освещение
+ }
+
+Давайте сделаем это
+
+Сначала давайте изменим наш фрагментный шейдер из
+[последней статьи](webgl-3d-lighting-point.html).
+
+```glsl
+#version 300 es
+precision highp float;
+
+// Переданный из вершинного шейдера.
+in vec3 v_normal;
+in vec3 v_surfaceToLight;
+in vec3 v_surfaceToView;
+
+uniform vec4 u_color;
+uniform float u_shininess;
++uniform vec3 u_lightDirection;
++uniform float u_limit; // в dot space
+
+// нам нужно объявить выход для фрагментного шейдера
+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 light = dot(normal, surfaceToLightDirection);
++ float light = 0.0;
+ float specular = 0.0;
+
++ float dotFromDirection = dot(surfaceToLightDirection,
++ -u_lightDirection);
++ if (dotFromDirection >= u_limit) {
+* light = dot(normal, surfaceToLightDirection);
+* if (light > 0.0) {
+* specular = pow(dot(normal, halfVector), u_shininess);
+* }
++ }
+
+ outColor = u_color;
+
+ // Давайте умножим только цветовую часть (не альфа)
+ // на свет
+ outColor.rgb *= light;
+
+ // Просто добавляем блик
+ outColor.rgb += specular;
+}
+```
+
+Конечно, нам нужно найти местоположения uniform переменных, которые мы
+только что добавили.
+
+```js
+ var lightDirection = [?, ?, ?];
+ var limit = degToRad(20);
+
+ ...
+
+ var lightDirectionLocation = gl.getUniformLocation(program, "u_lightDirection");
+ var limitLocation = gl.getUniformLocation(program, "u_limit");
+```
+
+и нам нужно их установить
+
+```js
+ gl.uniform3fv(lightDirectionLocation, lightDirection);
+ gl.uniform1f(limitLocation, Math.cos(limit));
+```
+
+И вот это
+
+{{{example url="../webgl-3d-lighting-spot.html" }}}
+
+Несколько вещей для заметки: Одна в том, что мы отрицаем `u_lightDirection` выше.
+Это [*шесть одного, полдюжины другого*](https://en.wiktionary.org/wiki/six_of_one,_half_a_dozen_of_the_other)
+тип вещи. Мы хотим, чтобы 2 направления, которые мы сравниваем, указывали в
+том же направлении, когда они совпадают. Это означает, что нам нужно сравнить
+surfaceToLightDirection с противоположным направлением прожектора.
+Мы могли бы сделать это многими разными способами. Мы могли бы передать отрицательное
+направление при установке uniform. Это был бы мой 1-й выбор,
+но я думал, что будет менее запутанно назвать uniform `u_lightDirection` вместо `u_reverseLightDirection` или `u_negativeLightDirection`
+
+Другая вещь, и, возможно, это просто личное предпочтение, я не
+люблю использовать условные операторы в шейдерах, если возможно. Я думаю, причина
+в том, что раньше шейдеры на самом деле не имели условных операторов. Если вы добавляли
+условный оператор, компилятор шейдера расширял код множеством
+умножений на 0 и 1 здесь и там, чтобы сделать так, чтобы не было
+никаких фактических условных операторов в коде. Это означало, что добавление условных операторов
+могло заставить ваш код взорваться в комбинаторные расширения. Я не
+уверен, что это все еще правда, но давайте избавимся от условных операторов в любом случае,
+просто чтобы показать некоторые техники. Вы можете сами решить, использовать их или нет.
+
+Есть функция GLSL, называемая `step`. Она принимает 2 значения, и если
+второе значение больше или равно первому, она возвращает 1.0. Иначе она возвращает 0. Вы могли бы написать это так в JavaScript
+
+ function step(a, b) {
+ if (b >= a) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+Давайте используем `step`, чтобы избавиться от условий
+
+```glsl
+ float dotFromDirection = dot(surfaceToLightDirection,
+ -u_lightDirection);
+ // inLight будет 1, если мы внутри прожектора, и 0, если нет
+ float inLight = step(u_limit, dotFromDirection);
+ float light = inLight * dot(normal, surfaceToLightDirection);
+ float specular = inLight * pow(dot(normal, halfVector), u_shininess);
+```
+
+Ничего не меняется визуально, но вот это
+
+{{{example url="../webgl-3d-lighting-spot-using-step.html" }}}
+
+Еще одна вещь в том, что сейчас прожектор очень резкий. Мы
+либо внутри прожектора, либо нет, и вещи просто становятся черными.
+
+Чтобы исправить это, мы могли бы использовать 2 предела вместо одного,
+внутренний предел и внешний предел.
+Если мы внутри внутреннего предела, то используем 1.0. Если мы снаружи внешнего
+предела, то используем 0.0. Если мы между внутренним пределом и внешним пределом,
+то интерполируем между 1.0 и 0.0.
+
+Вот один способ, как мы могли бы сделать это
+
+```glsl
+-uniform float u_limit; // в dot space
++uniform float u_innerLimit; // в dot space
++uniform float u_outerLimit; // в dot space
+
+...
+
+ float dotFromDirection = dot(surfaceToLightDirection,
+ -u_lightDirection);
+- float inLight = step(u_limit, dotFromDirection);
++ 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);
+
+```
+
+И это работает
+
+{{{example url="../webgl-3d-lighting-spot-falloff.html" }}}
+
+Теперь мы получаем что-то, что выглядит больше как прожектор!
+
+Одна вещь, о которой нужно знать, это если `u_innerLimit` равен `u_outerLimit`,
+то `limitRange` будет 0.0. Мы делим на `limitRange`, и деление на
+ноль плохо/не определено. Здесь нечего делать в шейдере, нам просто
+нужно убедиться в нашем JavaScript, что `u_innerLimit` никогда не равен
+`u_outerLimit`. (примечание: пример кода этого не делает).
+
+GLSL также имеет функцию, которую мы могли бы использовать для небольшого упрощения этого. Она
+называется `smoothstep`, и как `step` она возвращает значение от 0 до 1, но
+она принимает как нижнюю, так и верхнюю границу и интерполирует между 0 и 1 между
+этими границами.
+
+ smoothstep(lowerBound, upperBound, value)
+
+Давайте сделаем это
+
+```glsl
+ float dotFromDirection = dot(surfaceToLightDirection,
+ -u_lightDirection);
+- float limitRange = u_innerLimit - u_outerLimit;
+- float inLight = clamp((dotFromDirection - u_outerLimit) / limitRange, 0.0, 1.0);
+ float inLight = smoothstep(u_outerLimit, u_innerLimit, dotFromDirection);
+ float light = inLight * dot(normal, surfaceToLightDirection);
+ float specular = inLight * pow(dot(normal, halfVector), u_shininess);
+```
+
+Это тоже работает
+
+{{{example url="../webgl-3d-lighting-spot-falloff-using-smoothstep.html" }}}
+
+Разница в том, что `smoothstep` использует интерполяцию Эрмита вместо
+линейной интерполяции. Это означает, что между `lowerBound` и `upperBound`
+она интерполирует, как изображение ниже справа, тогда как линейная интерполяция, как изображение слева.
+
+
+
+Вам решать, имеет ли значение разница.
+
+Еще одна вещь, о которой нужно знать, это то, что функция `smoothstep` имеет неопределенные
+результаты, если `lowerBound` больше или равен `upperBound`. Иметь
+их равными - это та же проблема, что у нас была выше. Добавленная проблема не быть
+определенным, если `lowerBound` больше `upperBound`, новая, но для
+цели прожектора это никогда не должно быть правдой.
+
+
+
Будьте осторожны с неопределенным поведением в GLSL
+
+Несколько функций в GLSL не определены для определенных значений.
+Попытка возвести отрицательное число в степень с помощью pow - это один
+пример, поскольку результат был бы мнимым числом. Мы рассмотрели
+другой пример выше с smoothstep.
+
+Вам нужно попытаться быть в курсе этих или иначе ваши шейдеры будут
+получать разные результаты на разных машинах. Спецификация, в разделе
+8 перечисляет все встроенные функции, что они делают, и есть ли
+какое-либо неопределенное поведение.
+
Вот список неопределенных поведений. Обратите внимание, genType означает float, vec2, vec3 или vec4.
+
genType asin (genType x)
Арксинус. Возвращает угол, синус которого равен x. Диапазон
+значений, возвращаемых этой функцией, [−π/2, π/2]
+Результаты не определены, если ∣x∣ > 1.
+
+
+
genType acos (genType x)
Арккосинус. Возвращает угол, косинус которого равен x. Диапазон
+значений, возвращаемых этой функцией, [0, π].
+Результаты не определены, если ∣x∣ > 1.
+
+
+
+
genType atan (genType y, genType x)
Арктангенс. Возвращает угол, тангенс которого равен y/x. Знаки
+x и y используются для определения того, в каком квадранте находится
+угол. Диапазон значений, возвращаемых этой
+функцией, [−π,π]. Результаты не определены, если x и y
+оба равны 0.
+
+
genType acosh (genType x)
Аркгиперболический косинус; возвращает неотрицательный обратный
+к cosh. Результаты не определены, если x < 1.
+
+
genType atanh (genType x)
Аркгиперболический тангенс; возвращает обратный к tanh.
+Результаты не определены, если ∣x∣≥1.
+
+
genType pow (genType x, genType y)
Возвращает x, возведенный в степень y, т.е. xy.
+Результаты не определены, если x < 0.
+Результаты не определены, если x = 0 и y <= 0.
+
+
+
genType log (genType x)
Возвращает натуральный логарифм x, т.е. возвращает значение
+y, которое удовлетворяет уравнению x = ey.
+Результаты не определены, если x <= 0.
+
+
+
genType log2 (genType x)
Возвращает логарифм по основанию 2 от x, т.е. возвращает значение
+y, которое удовлетворяет уравнению x=2y.
+Результаты не определены, если x <= 0.
+
+
+
+
genType sqrt (genType x)
Возвращает √x .
+Результаты не определены, если x < 0.
+
+
+
genType inversesqrt (genType x)
+Возвращает 1/√x.
+Результаты не определены, если x <= 0.
+Возвращает 0.0, если x <= edge0, и 1.0, если x >= edge1, и
+выполняет плавную интерполяцию Эрмита между 0 и 1,
+когда edge0 < x < edge1. Это полезно в случаях, когда
+вы хотели бы пороговую функцию с плавным
+переходом. Это эквивалентно:
+
+
+ genType t;
+ t = clamp ((x – edge0) / (edge1 – edge0), 0, 1);
+ return t * t * (3 – 2 * t);
+
+
Результаты не определены, если edge0 >= edge1.
+
+
+
mat2 inverse(mat2 m)
+mat3 inverse(mat3 m)
+mat4 inverse(mat4 m)
+Возвращает матрицу, которая является обратной к m. Входная
+матрица m не изменяется. Значения в возвращенной
+матрице не определены, если m сингулярна или плохо обусловлена
+(почти сингулярна).
+
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-orthographic.md b/webgl/lessons/ru/webgl-3d-orthographic.md
new file mode 100644
index 000000000..97896e021
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-orthographic.md
@@ -0,0 +1,702 @@
+Title: WebGL2 - Ортографическая 3D
+Description: Как делать 3D в WebGL, начиная с ортографической проекции.
+TOC: Ортографическая 3D
+
+
+Этот пост является продолжением серии постов о WebGL.
+Первый [начался с основ](webgl-fundamentals.html) и
+предыдущий был [о 2D матрицах](webgl-2d-matrices.html).
+Если вы не читали их, пожалуйста, просмотрите их сначала.
+
+В последнем посте мы рассмотрели, как работают 2D матрицы. Мы говорили
+о том, как трансляция, вращение, масштабирование и даже проекция из
+пикселей в пространство отсечения могут быть выполнены одной матрицей и магической
+матричной математикой. Чтобы сделать 3D, нужно только небольшой шаг оттуда.
+
+В наших предыдущих 2D примерах у нас были 2D точки (x, y), которые мы умножали на
+матрицу 3x3. Чтобы сделать 3D, нам нужны 3D точки (x, y, z) и матрица 4x4.
+
+Давайте возьмем наш последний пример и изменим его на 3D. Мы снова используем F,
+но на этот раз 3D 'F'.
+
+Первое, что нам нужно сделать, это изменить вершинный шейдер для обработки 3D.
+Вот старый вершинный шейдер.
+
+```js
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec2 a_position;
+
+// Матрица для преобразования позиций
+uniform mat3 u_matrix;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
+}
+```
+
+А вот новый
+
+```glsl
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+*in vec4 a_position;
+
+// Матрица для преобразования позиций
+*uniform mat4 u_matrix;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Умножаем позицию на матрицу.
+* gl_Position = u_matrix * a_position;
+}
+```
+
+Он стал еще проще! Так же, как в 2D мы предоставляли `x` и `y`, а затем
+устанавливали `z` в 1, в 3D мы будем предоставлять `x`, `y` и `z`, и нам нужно, чтобы `w`
+был 1, но мы можем воспользоваться тем фактом, что для атрибутов
+`w` по умолчанию равен 1.
+
+Затем нам нужно предоставить 3D данные.
+
+```js
+ ...
+
+ // Говорим атрибуту, как получать данные из positionBuffer (ARRAY_BUFFER)
+* var size = 3; // 3 компонента на итерацию
+ var type = gl.FLOAT; // данные - это 32-битные числа с плавающей точкой
+ var normalize = false; // не нормализуем данные
+ var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой итерации, чтобы получить следующую позицию
+ var offset = 0; // начинаем с начала буфера
+ gl.vertexAttribPointer(
+ positionAttributeLocation, size, type, normalize, stride, offset);
+
+ ...
+
+ // Заполняем текущий буфер ARRAY_BUFFER
+ // значениями, которые определяют букву 'F'.
+ function setGeometry(gl) {
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array([
+ // левая колонка
+ 0, 0, 0,
+ 30, 0, 0,
+ 0, 150, 0,
+ 0, 150, 0,
+ 30, 0, 0,
+ 30, 150, 0,
+
+ // верхняя перекладина
+ 30, 0, 0,
+ 100, 0, 0,
+ 30, 30, 0,
+ 30, 30, 0,
+ 100, 0, 0,
+ 100, 30, 0,
+
+ // средняя перекладина
+ 30, 60, 0,
+ 67, 60, 0,
+ 30, 90, 0,
+ 30, 90, 0,
+ 67, 60, 0,
+ 67, 90, 0]),
+ gl.STATIC_DRAW);
+ }
+```
+
+Затем нам нужно изменить все матричные функции с 2D на 3D
+
+Вот 2D (до) версии m3.translation, m3.rotation и m3.scaling
+
+```js
+var m3 = {
+ translation: function translation(tx, ty) {
+ return [
+ 1, 0, 0,
+ 0, 1, 0,
+ tx, ty, 1
+ ];
+ },
+
+ rotation: function rotation(angleInRadians) {
+ var c = Math.cos(angleInRadians);
+ var s = Math.sin(angleInRadians);
+ return [
+ c,-s, 0,
+ s, c, 0,
+ 0, 0, 1
+ ];
+ },
+
+ scaling: function scaling(sx, sy) {
+ return [
+ sx, 0, 0,
+ 0, sy, 0,
+ 0, 0, 1
+ ];
+ },
+};
+```
+
+А вот обновленные 3D версии.
+
+```js
+var m4 = {
+ translation: function(tx, ty, tz) {
+ return [
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ tx, ty, tz, 1,
+ ];
+ },
+
+ xRotation: function(angleInRadians) {
+ var c = Math.cos(angleInRadians);
+ var s = Math.sin(angleInRadians);
+
+ return [
+ 1, 0, 0, 0,
+ 0, c, s, 0,
+ 0, -s, c, 0,
+ 0, 0, 0, 1,
+ ];
+ },
+
+ yRotation: function(angleInRadians) {
+ var c = Math.cos(angleInRadians);
+ var s = Math.sin(angleInRadians);
+
+ return [
+ c, 0, -s, 0,
+ 0, 1, 0, 0,
+ s, 0, c, 0,
+ 0, 0, 0, 1,
+ ];
+ },
+
+ zRotation: function(angleInRadians) {
+ var c = Math.cos(angleInRadians);
+ var s = Math.sin(angleInRadians);
+
+ return [
+ c, s, 0, 0,
+ -s, c, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ ];
+ },
+
+ scaling: function(sx, sy, sz) {
+ return [
+ sx, 0, 0, 0,
+ 0, sy, 0, 0,
+ 0, 0, sz, 0,
+ 0, 0, 0, 1,
+ ];
+ },
+};
+
+Обратите внимание, что теперь у нас есть 3 функции вращения. Нам нужна была только одна в 2D, так как мы
+эффективно вращались только вокруг оси Z. Теперь же, чтобы делать 3D, мы
+также хотим иметь возможность вращаться вокруг оси X и оси Y. Вы
+можете видеть, глядя на них, что они все очень похожи. Если бы мы
+их вывели, вы бы увидели, что они упрощаются точно так же, как раньше
+
+Вращение Z
+
+
+
newX = x * c + y * s;
+
newY = x * -s + y * c;
+
+
+Вращение Y
+
+
+
newX = x * c + z * s;
+
newZ = x * -s + z * c;
+
+
+Вращение X
+
+
+
newY = y * c + z * s;
+
newZ = y * -s + z * c;
+
+
+что дает вам эти вращения.
+
+
+
+Аналогично мы сделаем наши упрощенные функции
+
+```js
+ translate: function(m, tx, ty, tz) {
+ return m4.multiply(m, m4.translation(tx, ty, tz));
+ },
+
+ xRotate: function(m, angleInRadians) {
+ return m4.multiply(m, m4.xRotation(angleInRadians));
+ },
+
+ yRotate: function(m, angleInRadians) {
+ return m4.multiply(m, m4.yRotation(angleInRadians));
+ },
+
+ zRotate: function(m, angleInRadians) {
+ return m4.multiply(m, m4.zRotation(angleInRadians));
+ },
+
+ scale: function(m, sx, sy, sz) {
+ return m4.multiply(m, m4.scaling(sx, sy, sz));
+ },
+```
+
+И нам нужна функция умножения матриц 4x4
+
+```js
+ multiply: function(a, b) {
+ var b00 = b[0 * 4 + 0];
+ var b01 = b[0 * 4 + 1];
+ var b02 = b[0 * 4 + 2];
+ var b03 = b[0 * 4 + 3];
+ var b10 = b[1 * 4 + 0];
+ var b11 = b[1 * 4 + 1];
+ var b12 = b[1 * 4 + 2];
+ var b13 = b[1 * 4 + 3];
+ var b20 = b[2 * 4 + 0];
+ var b21 = b[2 * 4 + 1];
+ var b22 = b[2 * 4 + 2];
+ var b23 = b[2 * 4 + 3];
+ var b30 = b[3 * 4 + 0];
+ var b31 = b[3 * 4 + 1];
+ var b32 = b[3 * 4 + 2];
+ var b33 = b[3 * 4 + 3];
+ var a00 = a[0 * 4 + 0];
+ var a01 = a[0 * 4 + 1];
+ var a02 = a[0 * 4 + 2];
+ var a03 = a[0 * 4 + 3];
+ var a10 = a[1 * 4 + 0];
+ var a11 = a[1 * 4 + 1];
+ var a12 = a[1 * 4 + 2];
+ var a13 = a[1 * 4 + 3];
+ var a20 = a[2 * 4 + 0];
+ var a21 = a[2 * 4 + 1];
+ var a22 = a[2 * 4 + 2];
+ var a23 = a[2 * 4 + 3];
+ var a30 = a[3 * 4 + 0];
+ var a31 = a[3 * 4 + 1];
+ var a32 = a[3 * 4 + 2];
+ var a33 = a[3 * 4 + 3];
+
+ return [
+ b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
+ b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
+ b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
+ b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
+ b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
+ b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
+ b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
+ b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
+ b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
+ b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
+ b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
+ b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
+ b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
+ b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
+ b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
+ b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
+ ];
+ },
+```
+
+Нам также нужно обновить функцию проекции. Вот старая
+
+```js
+ projection: function (width, height) {
+ // Примечание: Эта матрица переворачивает ось Y, так что 0 находится сверху.
+ return [
+ 2 / width, 0, 0,
+ 0, -2 / height, 0,
+ -1, 1, 1
+ ];
+ },
+}
+```
+
+которая преобразовывала из пикселей в пространство отсечения. Для нашей первой попытки
+расширить её до 3D давайте попробуем
+
+```js
+ projection: function(width, height, depth) {
+ // Примечание: Эта матрица переворачивает ось Y, так что 0 находится сверху.
+ return [
+ 2 / width, 0, 0, 0,
+ 0, -2 / height, 0, 0,
+ 0, 0, 2 / depth, 0,
+ -1, 1, 0, 1,
+ ];
+ },
+```
+
+Так же, как нам нужно было преобразовать из пикселей в пространство отсечения для X и Y, для
+Z нам нужно сделать то же самое. В этом случае я делаю ось Z также в единицах пикселей.
+Я передам некоторое значение, аналогичное `width` для `depth`,
+так что наше пространство будет от 0 до `width` пикселей в ширину, от 0 до `height` пикселей в высоту, но
+для `depth` это будет от `-depth / 2` до `+depth / 2`.
+
+Наконец, нам нужно обновить код, который вычисляет матрицу.
+
+```js
+ // Вычисляем матрицу
+* var matrix = m4.projection(gl.canvas.clientWidth, gl.canvas.clientHeight, 400);
+* matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
+* matrix = m4.xRotate(matrix, rotation[0]);
+* matrix = m4.yRotate(matrix, rotation[1]);
+* matrix = m4.zRotate(matrix, rotation[2]);
+* matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
+
+ // Устанавливаем матрицу.
+* gl.uniformMatrix4fv(matrixLocation, false, matrix);
+```
+
+И вот этот пример.
+
+{{{example url="../webgl-3d-step1.html" }}}
+
+Первая проблема, которая у нас есть, заключается в том, что наша геометрия - это плоский F, что затрудняет
+видение любого 3D. Чтобы исправить это, давайте расширим геометрию до 3D. Наш
+текущий F состоит из 3 прямоугольников, по 2 треугольника каждый. Чтобы сделать его 3D, потребуется
+всего 16 прямоугольников. 3 прямоугольника спереди, 3 сзади,
+1 слева, 4 справа, 2 сверху, 3 снизу.
+
+
+
+Это довольно много, чтобы перечислить здесь.
+16 прямоугольников с 2 треугольниками на прямоугольник и 3 вершинами на треугольник - это 96
+вершин. Если вы хотите увидеть все из них, просмотрите исходный код примера.
+
+Нам нужно рисовать больше вершин, поэтому
+
+```js
+ // Рисуем геометрию.
+ var primitiveType = gl.TRIANGLES;
+ var offset = 0;
+* var count = 16 * 6;
+ gl.drawArrays(primitiveType, offset, count);
+```
+
+И вот эта версия
+
+{{{example url="../webgl-3d-step2.html" }}}
+
+Перемещая ползунки, довольно трудно сказать, что это 3D. Давайте попробуем
+раскрасить каждый прямоугольник в разный цвет. Для этого мы добавим еще один
+атрибут к нашему вершинному шейдеру и varying для передачи его из вершинного
+шейдера в фрагментный шейдер.
+
+Вот новый вершинный шейдер
+
+```glsl
+#version 300 es
+
+// атрибут - это вход (in) в вершинный шейдер.
+// Он будет получать данные из буфера
+in vec4 a_position;
++in vec4 a_color;
+
+// Матрица для преобразования позиций
+uniform mat4 u_matrix;
+
++// varying для передачи цвета в фрагментный шейдер
++out vec4 v_color;
+
+// все шейдеры имеют основную функцию
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_matrix * a_position;
+
++ // Передаем цвет в фрагментный шейдер.
++ v_color = a_color;
+}
+```
+
+И нам нужно использовать этот цвет в фрагментном шейдере
+
+```glsl
+#version 300 es
+
+precision highp float;
+
++// varying цвет, переданный из вершинного шейдера
++in vec4 v_color;
+
+// нам нужно объявить выход для фрагментного шейдера
+out vec4 outColor;
+
+void main() {
+* outColor = v_color;
+}
+```
+
+Нам нужно найти местоположение атрибута для предоставления цветов, затем настроить другой
+буфер и атрибут для предоставления цветов.
+
+```js
+ ...
+ var colorAttributeLocation = gl.getAttribLocation(program, "a_color");
+
+ ...
+
+ // создаем буфер цветов, делаем его текущим ARRAY_BUFFER
+ // и копируем значения цветов
+ var colorBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
+ setColors(gl);
+
+ // Включаем атрибут
+ gl.enableVertexAttribArray(colorAttributeLocation);
+
+ // Говорим атрибуту, как получать данные из colorBuffer (ARRAY_BUFFER)
+ var size = 3; // 3 компонента на итерацию
+ var type = gl.UNSIGNED_BYTE; // данные - это 8-битные беззнаковые байты
+ var normalize = true; // преобразуем из 0-255 в 0.0-1.0
+ var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой
+ // итерации, чтобы получить следующий цвет
+ var offset = 0; // начинаем с начала буфера
+ gl.vertexAttribPointer(
+ colorAttributeLocation, size, type, normalize, stride, offset);
+
+ ...
+
+// Заполняем буфер цветами для 'F'.
+
+function setColors(gl) {
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Uint8Array([
+ // левая колонка спереди
+ 200, 70, 120,
+ 200, 70, 120,
+ 200, 70, 120,
+ 200, 70, 120,
+ 200, 70, 120,
+ 200, 70, 120,
+
+ // верхняя перекладина спереди
+ 200, 70, 120,
+ 200, 70, 120,
+ ...
+ ...
+ gl.STATIC_DRAW);
+}
+```
+
+Теперь мы получаем это.
+
+{{{example url="../webgl-3d-step3.html" }}}
+
+Ой, что это за беспорядок? Ну, оказывается, все различные части
+этого 3D 'F', передняя, задняя, боковые и т.д., рисуются в том порядке, в котором они появляются в
+наших геометрических данных. Это не дает нам вполне желаемых результатов, так как иногда
+те, что сзади, рисуются после тех, что спереди.
+
+
+
+Красноватая часть - это
+**передняя** часть 'F', но поскольку это первая часть наших данных,
+она рисуется первой, а затем другие треугольники за ней рисуются
+после, покрывая её. Например, фиолетовая часть
+на самом деле задняя часть 'F'. Она рисуется 2-й, потому что приходит 2-й в наших данных.
+
+Треугольники в WebGL имеют концепцию лицевой и обратной стороны. По умолчанию
+лицевой треугольник имеет свои вершины в направлении против часовой стрелки.
+Обратный треугольник имеет свои вершины в направлении по часовой стрелке.
+
+
+
+WebGL имеет возможность рисовать только лицевые или обратные
+треугольники. Мы можем включить эту функцию с помощью
+
+```js
+ gl.enable(gl.CULL_FACE);
+```
+
+Хорошо, поместим это в нашу функцию `drawScene`. С этой
+функцией включенной, WebGL по умолчанию "отсекает" обратные треугольники.
+"Отсекание" в данном случае - это модное слово для "не рисования".
+
+Обратите внимание, что насколько WebGL обеспокоен, является ли треугольник
+идущим по часовой стрелке или против часовой стрелки, зависит от
+вершин этого треугольника в пространстве отсечения. Другими словами, WebGL выясняет,
+является ли треугольник лицевым или обратным ПОСЛЕ того, как вы применили математику к
+вершинам в вершинном шейдере. Это означает, например, что треугольник по часовой стрелке,
+который масштабируется по X на -1, становится треугольником против часовой стрелки, или
+треугольник по часовой стрелке, повернутый на 180 градусов, становится треугольником против часовой стрелки.
+Поскольку у нас была отключена CULL_FACE, мы можем видеть как
+треугольники по часовой стрелке (лицевые), так и против часовой стрелки (обратные). Теперь, когда мы
+включили её, всякий раз, когда лицевой треугольник переворачивается либо из-за
+масштабирования, либо вращения, либо по какой-либо другой причине, WebGL не будет его рисовать.
+Это хорошая вещь, поскольку когда вы поворачиваете что-то в 3D, вы
+обычно хотите, чтобы любые треугольники, обращенные к вам, считались лицевыми.
+
+С включенной CULL_FACE мы получаем это
+
+{{{example url="../webgl-3d-step4.html" }}}
+
+Эй! Куда делись все треугольники? Оказывается, многие из них
+смотрят в неправильную сторону. Поверните его, и вы увидите, как они появляются, когда вы смотрите
+с другой стороны. К счастью, это легко исправить. Мы просто смотрим на те,
+которые обращены назад, и меняем местами 2 их вершины. Например, если один
+обратный треугольник
+
+```
+ 1, 2, 3,
+ 40, 50, 60,
+ 700, 800, 900,
+```
+
+мы просто меняем местами последние 2 вершины, чтобы сделать его лицевым.
+
+```
+ 1, 2, 3,
+* 700, 800, 900,
+* 40, 50, 60,
+```
+
+Проходя и исправляя все обратные треугольники, мы получаем это
+
+{{{example url="../webgl-3d-step5.html" }}}
+
+Это ближе, но все еще есть еще одна проблема. Даже со всеми
+треугольниками, обращенными в правильном направлении, и с отсечением обратных,
+у нас все еще есть места, где треугольники, которые должны быть сзади,
+рисуются поверх треугольников, которые должны быть спереди.
+
+Введите БУФЕР ГЛУБИНЫ.
+
+Буфер глубины, иногда называемый Z-буфером, - это прямоугольник пикселей *глубины*,
+один пиксель глубины для каждого цветного пикселя, используемого для создания изображения. Когда
+WebGL рисует каждый цветной пиксель, он также может рисовать пиксель глубины. Он делает это
+на основе значений, которые мы возвращаем из вершинного шейдера для Z. Так же, как мы
+должны были преобразовать в пространство отсечения для X и Y, Z также находится в пространстве отсечения (от -1
+до +1). Это значение затем преобразуется в значение пространства глубины (от 0 до +1).
+Перед тем как WebGL нарисует цветной пиксель, он проверит соответствующий пиксель глубины.
+Если значение глубины для пикселя, который он собирается нарисовать, больше
+значения соответствующего пикселя глубины, то WebGL не рисует
+новый цветной пиксель. В противном случае он рисует как новый цветной пиксель с
+цветом из вашего фрагментного шейдера, ТАК И новый пиксель глубины с новым
+значением глубины. Это означает, что пиксели, которые находятся за другими пикселями, не будут
+нарисованы.
+
+Мы можем включить эту функцию почти так же просто, как мы включили отсечение с помощью
+
+```js
+ gl.enable(gl.DEPTH_TEST);
+```
+
+
+Нам также нужно очистить буфер глубины обратно до 1.0 перед тем, как мы начнем рисовать.
+
+```js
+ // Рисуем сцену.
+ function drawScene() {
+
+ ...
+
+ // Очищаем холст И буфер глубины.
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+ ...
+```
+
+И теперь мы получаем
+
+{{{example url="../webgl-3d-step6.html" }}}
+
+что является 3D!
+
+Одна небольшая вещь. В большинстве 3D математических библиотек нет функции `projection` для
+выполнения наших преобразований из пространства отсечения в пространство пикселей. Скорее обычно есть функция
+называемая `ortho` или `orthographic`, которая выглядит так
+
+ var m4 = {
+ orthographic: function(left, right, bottom, top, near, far) {
+ return [
+ 2 / (right - left), 0, 0, 0,
+ 0, 2 / (top - bottom), 0, 0,
+ 0, 0, 2 / (near - far), 0,
+
+ (left + right) / (left - right),
+ (bottom + top) / (bottom - top),
+ (near + far) / (near - far),
+ 1,
+ ];
+ }
+
+В отличие от нашей упрощенной функции `projection` выше, которая имела только параметры width, height и depth,
+эта более распространенная ортографическая функция проекции позволяет нам передать left, right,
+bottom, top, near и far, что дает нам больше гибкости. Чтобы использовать её так же, как
+нашу оригинальную функцию проекции, мы бы вызвали её с
+
+ var left = 0;
+ var right = gl.canvas.clientWidth;
+ var bottom = gl.canvas.clientHeight;
+ var top = 0;
+ var near = 200;
+ var far = -200;
+ m4.orthographic(left, right, bottom, top, near, far);
+
+В следующем посте я расскажу о [том, как сделать перспективу](webgl-3d-perspective.html).
+
+
+
Почему атрибут vec4, но gl.vertexAttribPointer size 3
+
+Для тех из вас, кто внимателен к деталям, вы могли заметить, что мы определили наши 2 атрибута как
+
+
+in vec4 a_position;
+in vec4 a_color;
+
+
оба из которых 'vec4', но когда мы говорим WebGL, как получать данные из наших буферов, мы использовали
+
{{#escapehtml}}
+// Говорим атрибуту, как получать данные из positionBuffer (ARRAY_BUFFER)
+var size = 3; // 3 компонента на итерацию
+var type = gl.FLOAT; // данные - это 32-битные числа с плавающей точкой
+var normalize = false; // не нормализуем данные
+var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой
+ // итерации, чтобы получить следующую позицию
+var offset = 0; // начинаем с начала буфера
+gl.vertexAttribPointer(
+ positionAttributeLocation, size, type, normalize, stride, offset);
+
+...
+// Говорим атрибуту, как получать данные из colorBuffer (ARRAY_BUFFER)
+var size = 3; // 3 компонента на итерацию
+var type = gl.UNSIGNED_BYTE; // данные - это 8-битные беззнаковые байты
+var normalize = true; // преобразуем из 0-255 в 0.0-1.0
+var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой
+ // итерации, чтобы получить следующий цвет
+var offset = 0; // начинаем с начала буфера
+gl.vertexAttribPointer(
+ colorAttributeLocation, size, type, normalize, stride, offset);
+{{/escapehtml}}
+
+Эта '3' в каждом из них говорит только извлекать 3 значения из буфера на атрибут
+на итерацию вершинного шейдера.
+Это работает, потому что в вершинном шейдере WebGL предоставляет значения по умолчанию для тех
+значений, которые вы не предоставляете. Значения по умолчанию - 0, 0, 0, 1, где x = 0, y = 0, z = 0
+и w = 1. Вот почему в нашем старом 2D вершинном шейдере нам приходилось явно
+предоставлять 1. Мы передавали x и y, и нам нужна была 1 для z, но
+поскольку значение по умолчанию для z равно 0, нам приходилось явно предоставлять 1. Для 3D
+же, хотя мы не предоставляем 'w', он по умолчанию равен 1, что и нужно
+для работы матричной математики.
+
+
+```
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-perspective-correct-texturemapping.md b/webgl/lessons/ru/webgl-3d-perspective-correct-texturemapping.md
new file mode 100644
index 000000000..58ff33afd
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-perspective-correct-texturemapping.md
@@ -0,0 +1,329 @@
+Title: WebGL2 3D Перспективно-корректное наложение текстур
+Description: Что особенного в W
+TOC: Перспективно-корректное наложение текстур
+
+
+Эта статья является продолжением серии статей о WebGL. Первая
+[началась с основ](webgl-fundamentals.html). Эта статья
+покрывает перспективно-корректное наложение текстур. Для понимания вам
+вероятно нужно прочитать о [перспективной проекции](webgl-3d-perspective.html) и возможно [текстурировании](webgl-3d-textures.html)
+также. Вам также нужно знать о [varying и что они делают](webgl-how-it-works.html), но я кратко расскажу о них здесь.
+
+Итак, в статье "[как это работает](webgl-how-it-works.html)"
+мы рассмотрели как работают varying. Вершинный шейдер может объявить
+varying и установить ему какое-то значение. После того как вершинный шейдер был вызван
+3 раза WebGL нарисует треугольник. Пока он рисует этот треугольник
+для каждого пикселя он вызовет наш фрагментный шейдер и спросит какой
+цвет сделать для этого пикселя. Между 3 вершинами треугольника
+он передаст нам наши varying интерполированные между 3 значениями.
+
+{{{diagram url="resources/fragment-shader-anim.html" width="600" height="400" caption="v_color интерполируется между v0, v1 и v2" }}}
+
+Возвращаясь к нашей [первой статье](webgl-fundamentals.html) мы нарисовали треугольник в
+clip space, без математики. Мы просто передали некоторые clip space координаты
+в простой вершинный шейдер, который выглядел так
+
+ #version 300 es
+
+ // атрибут это вход (in) в вершинный шейдер.
+ // Он будет получать данные из буфера
+ in vec4 a_position;
+
+ // все шейдеры имеют основную функцию
+ void main() {
+
+ // gl_Position это специальная переменная, которую вершинный шейдер
+ // отвечает за установку
+ gl_Position = a_position;
+ }
+
+У нас был простой фрагментный шейдер, который рисует постоянный цвет
+
+ #version 300 es
+
+ // фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно
+ // выбрать одну. highp это хороший выбор по умолчанию
+ precision highp float;
+
+ // нам нужно объявить выход для фрагментного шейдера
+ out vec4 outColor;
+
+ void main() {
+ // Просто установим выход на постоянный красно-фиолетовый
+ outColor = vec4(1, 0, 0.5, 1);
+ }
+
+Итак, давайте сделаем так, чтобы он рисовал 2 прямоугольника в clip space. Мы передадим ему эти
+данные с `X`, `Y`, `Z`, и `W` для каждой вершины.
+
+ var positions = [
+ -.8, -.8, 0, 1, // 1й прямоугольник 1й треугольник
+ .8, -.8, 0, 1,
+ -.8, -.2, 0, 1,
+ -.8, -.2, 0, 1, // 1й прямоугольник 2й треугольник
+ .8, -.8, 0, 1,
+ .8, -.2, 0, 1,
+
+ -.8, .2, 0, 1, // 2й прямоугольник 1й треугольник
+ .8, .2, 0, 1,
+ -.8, .8, 0, 1,
+ -.8, .8, 0, 1, // 2й прямоугольник 2й треугольник
+ .8, .2, 0, 1,
+ .8, .8, 0, 1,
+ ];
+
+Вот это
+
+{{{example url="../webgl-clipspace-rectangles.html" }}}
+
+Давайте добавим один varying float. Мы передадим это напрямую
+из вершинного шейдера в фрагментный шейдер.
+
+ #version 300 es
+
+ in vec4 a_position;
+ + in float a_brightness;
+
+ + out float v_brightness;
+
+ void main() {
+ gl_Position = a_position;
+
+ + // просто передаем яркость в фрагментный шейдер
+ + v_brightness = a_brightness;
+ }
+
+В фрагментном шейдере мы будем использовать этот varying для установки цвета
+
+ #version 300 es
+
+ precision highp float;
+
+ + // передается из вершинного шейдера и интерполируется
+ + in float v_brightness;
+
+ // нам нужно объявить выход для фрагментного шейдера
+ out vec4 outColor;
+
+ void main() {
+ * outColor = vec4(v_brightness, 0, 0, 1); // красные
+ }
+
+Нам нужно предоставить данные для этого varying, поэтому мы создадим буфер и
+поместим туда некоторые данные. Одно значение на вершину. Мы установим все значения яркости
+для вершин слева в 0, а тех что справа в 1.
+
+```
+ // Создаем буфер и помещаем 12 значений яркости в него
+ var brightnessBuffer = gl.createBuffer();
+
+ // Привязываем его к ARRAY_BUFFER (думайте об этом как ARRAY_BUFFER = brightnessBuffer)
+ gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);
+
+ var brightness = [
+ 0, // 1й прямоугольник 1й треугольник
+ 1,
+ 0,
+ 0, // 1й прямоугольник 2й треугольник
+ 1,
+ 1,
+
+ 0, // 2й прямоугольник 1й треугольник
+ 1,
+ 0,
+ 0, // 2й прямоугольник 2й треугольник
+ 1,
+ 1,
+ ];
+
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(brightness), gl.STATIC_DRAW);
+```
+
+Нам также нужно найти местоположение атрибута `a_brightness`
+во время инициализации
+
+```
+ // ищем куда должны идти данные вершин.
+ var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
++ var brightnessAttributeLocation = gl.getAttribLocation(program, "a_brightness");
+```
+
+и настроить этот атрибут во время рендеринга
+
+```
+ // Включаем атрибут
+ gl.enableVertexAttribArray(brightnessAttributeLocation);
+
+ // Привязываем буфер позиций.
+ gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);
+
+ // Говорим атрибуту как получать данные из brightnessBuffer (ARRAY_BUFFER)
+ var size = 1; // 1 компонент на итерацию
+ var type = gl.FLOAT; // данные это 32-битные float
+ var normalize = false; // не нормализуем данные
+ var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) каждую итерацию чтобы получить следующую позицию
+ var offset = 0; // начинаем с начала буфера
+ gl.vertexAttribPointer(
+ brightnessAttributeLocation, size, type, normalize, stride, offset);
+```
+
+И теперь когда мы рендерим мы получаем два прямоугольника, которые черные слева
+когда `brightness` равен 0 и красные справа когда `brightness` равен 1 и
+для области между `brightness` интерполируется или (варьируется) когда
+он проходит через треугольники.
+
+{{{example url="../webgl-clipspace-rectangles-with-varying.html" }}}
+
+Итак, из [статьи о перспективе](webgl-3d-perspective.html) мы знаем, что WebGL берет любое значение, которое мы помещаем в `gl_Position` и делит его на
+`gl_Position.w`.
+
+В вершинах выше мы предоставили `1` для `W`, но поскольку мы знаем, что WebGL
+будет делить на `W`, то мы должны быть в состоянии сделать что-то вроде этого
+и получить тот же результат.
+
+```
+ var mult = 20;
+ var positions = [
+ -.8, .8, 0, 1, // 1й прямоугольник 1й треугольник
+ .8, .8, 0, 1,
+ -.8, .2, 0, 1,
+ -.8, .2, 0, 1, // 1й прямоугольник 2й треугольник
+ .8, .8, 0, 1,
+ .8, .2, 0, 1,
+
+ -.8 , -.2 , 0, 1, // 2й прямоугольник 1й треугольник
+ .8 * mult, -.2 * mult, 0, mult,
+ -.8 , -.8 , 0, 1,
+ -.8 , -.8 , 0, 1, // 2й прямоугольник 2й треугольник
+ .8 * mult, -.2 * mult, 0, mult,
+ .8 * mult, -.8 * mult, 0, mult,
+ ];
+```
+
+Выше вы можете видеть, что для каждой точки справа во втором
+прямоугольнике мы умножаем `X` и `Y` на `mult`, но мы также
+устанавливаем `W` в `mult`. Поскольку WebGL будет делить на `W`, мы должны получить
+точно такой же результат, верно?
+
+Ну вот это
+
+{{{example url="../webgl-clipspace-rectangles-with-varying-non-1-w.html" }}}
+
+Обратите внимание, что 2 прямоугольника нарисованы в том же месте, где они были раньше. Это
+доказывает, что `X * MULT / MULT(W)` все еще просто `X` и то же самое для `Y`. Но цвета
+другие. Что происходит?
+
+Оказывается, WebGL использует `W` для реализации перспективно-корректного
+наложения текстур или, скорее, для перспективно-корректной интерполяции
+varying.
+
+Фактически, чтобы было легче увидеть, давайте взломаем фрагментный шейдер до этого
+
+ outColor = vec4(fract(v_brightness * 10.), 0, 0, 1); // красные
+
+умножение `v_brightness` на 10 заставит значение идти от 0 до 10. `fract` будет
+просто держать дробную часть, так что оно будет идти 0 до 1, 0 до 1, 0 до 1, 10 раз.
+
+{{{example url="../webgl-clipspace-rectangles-with-varying-non-1-w-repeat.html" }}}
+
+Линейная интерполяция от одного значения к другому была бы этой
+формулой
+
+ result = (1 - t) * a + t * b
+
+Где `t` это значение от 0 до 1, представляющее некоторую позицию между `a` и `b`. 0 в `a` и 1 в `b`.
+
+Для varying, однако, WebGL использует эту формулу
+
+ result = (1 - t) * a / aW + t * b / bW
+ -----------------------------
+ (1 - t) / aW + t / bW
+
+Где `aW` это `W`, который был установлен на `gl_Position.w`, когда varying был
+установлен в `a`, и `bW` это `W`, который был установлен на `gl_Position.w`, когда
+varying был установлен в `b`.
+
+Почему это важно? Ну, вот простой текстурированный куб, как мы закончили в [статье о текстурах](webgl-3d-textures.html). Я настроил
+UV координаты, чтобы они шли от 0 до 1 на каждой стороне, и он использует 4x4 пиксельную текстуру.
+
+{{{example url="../webgl-perspective-correct-cube.html" }}}
+
+Теперь давайте возьмем этот пример и изменим вершинный шейдер так, чтобы
+мы делили на `W` сами. Нам просто нужно добавить 1 строку.
+
+```
+#version 300 es
+
+in vec4 a_position;
+in vec2 a_texcoord;
+
+uniform mat4 u_matrix;
+
+out vec2 v_texcoord;
+
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_matrix * a_position;
+
++ // Вручную делим на W.
++ gl_Position /= gl_Position.w;
+
+ // Передаем texcoord в фрагментный шейдер.
+ v_texcoord = a_texcoord;
+}
+```
+
+Деление на `W` означает, что `gl_Position.w` в итоге будет 1.
+`X`, `Y`, и `Z` выйдут точно так же, как если бы мы позволили
+WebGL сделать деление за нас. Ну, вот результаты.
+
+{{{example url="../webgl-non-perspective-correct-cube.html" }}}
+
+Мы все еще получаем 3D куб, но текстуры искажаются. Это
+потому что, не передавая `W` как было раньше, WebGL не может сделать
+перспективно-корректное наложение текстур. Или более правильно, WebGL не может
+сделать перспективно-корректную интерполяцию varying.
+
+Если вы помните, `W` был нашим
+значением `Z` из нашей [матрицы перспективы](webgl-3d-perspective.html)).
+С `W` просто равным `1` WebGL просто в итоге делает линейную интерполяцию.
+Фактически, если вы возьмете уравнение выше
+
+ result = (1 - t) * a / aW + t * b / bW
+ -----------------------------
+ (1 - t) / aW + t / bW
+
+И измените все `W` на 1, мы получим
+
+ result = (1 - t) * a / 1 + t * b / 1
+ ---------------------------
+ (1 - t) / 1 + t / 1
+
+Деление на 1 ничего не делает, поэтому мы можем упростить до этого
+
+ result = (1 - t) * a + t * b
+ -------------------
+ (1 - t) + t
+
+`(1 - t) + t` когда `t` идет от 0 до 1 это то же самое что `1`. Например
+если `t` был `.7` мы получили бы `(1 - .7) + .7` что есть `.3 + .7` что есть `1`. Другими словами мы можем убрать низ, так что мы остаемся с
+
+ result = (1 - t) * a + t * b
+
+Что то же самое что уравнение линейной интерполяции выше.
+
+Надеюсь, теперь ясно, почему WebGL использует матрицу 4x4 и
+4-значные векторы с `X`, `Y`, `Z`, и `W`. `X` и `Y` деленные на `W` получают clip space координату. `Z` деленный на `W` также получает clipspace координату в Z, а `W` все еще используется во время интерполяции varying и
+предоставляет возможность делать перспективно-корректное наложение текстур.
+
+
+
Игровые консоли середины 1990-х
+
+Как небольшая деталь, PlayStation 1 и некоторые другие
+игровые консоли той же эпохи не делали перспективно-корректное наложение текстур. Глядя на результаты выше, вы теперь можете увидеть, почему
+они выглядели так, как выглядели.
+
+
+
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-perspective.md b/webgl/lessons/ru/webgl-3d-perspective.md
new file mode 100644
index 000000000..2d4b468e8
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-perspective.md
@@ -0,0 +1,406 @@
+Title: WebGL2 3D Перспектива
+Description: Как отобразить перспективу в 3D в WebGL
+TOC: 3D Перспектива
+
+
+Этот пост является продолжением серии постов о WebGL.
+Первый [начался с основ](webgl-fundamentals.html) и
+предыдущий был о [3D Основы](webgl-3d-orthographic.html).
+Если вы не читали их, пожалуйста, просмотрите их сначала.
+
+В последнем посте мы рассмотрели, как делать 3D, но этот 3D не имел никакой перспективы.
+Он использовал то, что называется "ортографическим" видом, который имеет свои применения, но это
+обычно не то, что люди хотят, когда говорят "3D".
+
+Вместо этого нам нужно добавить перспективу. Что такое перспектива?
+Это в основном особенность, что вещи, которые находятся дальше, выглядят
+меньше.
+
+
+
+Глядя на пример выше, мы видим, что вещи дальше
+рисуются меньше. Учитывая наш текущий пример, один простой способ
+сделать так, чтобы вещи, которые находятся дальше, выглядели меньше,
+было бы разделить clip space X и Y на Z.
+
+Думайте об этом так: Если у вас есть линия от (10, 15) до (20,15),
+она длиной 10 единиц. В нашем текущем примере она была бы нарисована длиной 10 пикселей.
+Но если мы разделим на Z, то, например, если Z равен 1
+
+
+10 / 1 = 10
+20 / 1 = 20
+abs(10-20) = 10
+
+
+она была бы длиной 10 пикселей. Если Z равен 2, она была бы
+
+
+
+Вы можете видеть, что по мере увеличения Z, по мере того, как он становится дальше, мы в конечном итоге рисуем его меньше.
+Если мы разделим в clip space, мы можем получить лучшие результаты, потому что Z будет меньшим числом (-1 до +1).
+Если мы добавим fudgeFactor, чтобы умножить Z перед тем, как мы разделим, мы можем настроить, насколько меньше вещи
+становятся для данного расстояния.
+
+Давайте попробуем это. Сначала давайте изменим вершинный шейдер, чтобы разделить на Z после того, как мы
+умножили его на наш "fudgeFactor".
+
+```
+...
++uniform float u_fudgeFactor;
+...
+void main() {
+ // Умножаем позицию на матрицу.
+* vec4 position = u_matrix * a_position;
+
+ // Настраиваем z для деления на
++ float zToDivideBy = 1.0 + position.z * u_fudgeFactor;
+
+ // Делим x и y на z.
+* gl_Position = vec4(position.xy / zToDivideBy, position.zw);
+}
+```
+
+Обратите внимание, поскольку Z в clip space идет от -1 до +1, я добавил 1, чтобы получить
+`zToDivideBy`, чтобы он шел от 0 до +2 * fudgeFactor
+
+Нам также нужно обновить код, чтобы позволить нам установить fudgeFactor.
+
+```
+ ...
++ var fudgeLocation = gl.getUniformLocation(program, "u_fudgeFactor");
+
+ ...
++ var fudgeFactor = 1;
+ ...
+ function drawScene() {
+ ...
++ // Устанавливаем fudgeFactor
++ gl.uniform1f(fudgeLocation, fudgeFactor);
+
+ // Рисуем геометрию.
+ gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
+```
+
+И вот результат.
+
+{{{example url="../webgl-3d-perspective.html" }}}
+
+Если это не ясно, перетащите слайдер "fudgeFactor" с 1.0 на 0.0, чтобы увидеть,
+как вещи выглядели раньше, прежде чем мы добавили наш код деления на Z.
+
+
+
ортографическая vs перспективная
+
+Оказывается, WebGL берет значение x,y,z,w, которое мы присваиваем `gl_Position` в нашем вершинном
+шейдере, и автоматически делит его на w.
+
+Мы можем доказать это очень легко, изменив шейдер и вместо того, чтобы делать
+деление сами, поместить `zToDivideBy` в `gl_Position.w`.
+
+```
+...
+uniform float u_fudgeFactor;
+...
+void main() {
+ // Умножаем позицию на матрицу.
+ vec4 position = u_matrix * a_position;
+
+ // Настраиваем z для деления на
+ float zToDivideBy = 1.0 + position.z * u_fudgeFactor;
+
+ // Делим x, y и z на zToDivideBy
+ gl_Position = vec4(position.xyz, zToDivideBy);
+}
+```
+
+и посмотреть, как это точно то же самое.
+
+{{{example url="../webgl-3d-perspective-w.html" }}}
+
+Почему тот факт, что WebGL автоматически делит на W, полезен? Потому что теперь, используя
+больше матричной магии, мы можем просто использовать еще одну матрицу, чтобы скопировать z в w.
+
+Матрица, как эта
+
+
+
+Итак, давайте снова изменим программу, чтобы просто использовать матрицы.
+
+Сначала давайте вернем вершинный шейдер. Он снова простой
+
+```
+uniform mat4 u_matrix;
+
+void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_matrix * a_position;
+ ...
+}
+```
+
+Далее давайте сделаем функцию для создания нашей матрицы Z → W.
+
+```
+function makeZToWMatrix(fudgeFactor) {
+ return [
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, fudgeFactor,
+ 0, 0, 0, 1,
+ ];
+}
+```
+
+и мы изменим код, чтобы использовать ее.
+
+```
+ ...
+
+ // Вычисляем матрицу
++ var matrix = makeZToWMatrix(fudgeFactor);
+* matrix = m4.multiply(matrix, m4.projection(gl.canvas.clientWidth, gl.canvas.clientHeight, 400));
+ matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
+ matrix = m4.xRotate(matrix, rotation[0]);
+ matrix = m4.yRotate(matrix, rotation[1]);
+ matrix = m4.zRotate(matrix, rotation[2]);
+ matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
+
+ ...
+```
+
+и обратите внимание, снова, это точно то же самое.
+
+{{{example url="../webgl-3d-perspective-w-matrix.html" }}}
+
+Все это было в основном просто чтобы показать вам, что деление на Z дает нам перспективу
+и что WebGL удобно делает это деление на Z для нас.
+
+Но все еще есть некоторые проблемы. Например, если вы установите Z примерно на -100, вы увидите что-то вроде
+анимации ниже
+
+
+
+Что происходит? Почему F исчезает рано? Так же, как WebGL обрезает X и Y до
+значений между +1 и -1, он также обрезает Z. То, что мы видим здесь, это где Z < -1.
+
+Я мог бы вдаваться в детали математики, чтобы исправить это, но [вы можете вывести это](https://stackoverflow.com/a/28301213/128511) так же, как
+мы делали 2D проекцию. Нам нужно взять Z, добавить некоторое количество и масштабировать некоторое количество, и мы можем сделать любой диапазон, который мы хотим,
+перемаппированным в -1 до +1.
+
+Крутая вещь в том, что все эти шаги могут быть сделаны в 1 матрице. Еще лучше, вместо `fudgeFactor`
+мы решим на `fieldOfView` и вычислим правильные значения, чтобы это произошло.
+
+Вот функция для построения матрицы.
+
+```
+var m4 = {
+ perspective: function(fieldOfViewInRadians, aspect, near, far) {
+ var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians);
+ var rangeInv = 1.0 / (near - far);
+
+ return [
+ f / aspect, 0, 0, 0,
+ 0, f, 0, 0,
+ 0, 0, (near + far) * rangeInv, -1,
+ 0, 0, near * far * rangeInv * 2, 0
+ ];
+ },
+
+ ...
+```
+
+Эта матрица сделает все наши преобразования для нас. Она настроит единицы так, чтобы они были
+в clip space, она сделает математику так, чтобы мы могли выбрать поле зрения по углу,
+и она позволит нам выбрать наше Z-обрезающее пространство. Она предполагает, что есть *глаз* или *камера* в
+начале координат (0, 0, 0), и учитывая `zNear` и `fieldOfView`, она вычисляет, что потребуется, чтобы
+вещи на `zNear` оказались на `Z = -1`, а вещи на `zNear`, которые составляют половину `fieldOfView` выше или ниже центра,
+оказались с `Y = -1` и `Y = 1` соответственно. Она вычисляет, что использовать для X, просто умножая на переданный `aspect`.
+Мы обычно устанавливаем это в `width / height` области отображения.
+Наконец, она выясняет, насколько масштабировать вещи в Z, чтобы вещи на zFar оказались на `Z = 1`.
+
+Вот диаграмма матрицы в действии.
+
+{{{diagram url="../frustum-diagram.html" width="400" height="600" }}}
+
+Эта форма, которая выглядит как 4-сторонний конус, в котором вращаются кубы, называется "усеченной пирамидой".
+Матрица берет пространство внутри усеченной пирамиды и преобразует это в clip space. `zNear` определяет, где
+вещи будут обрезаны спереди, а `zFar` определяет, где вещи обрезаются сзади. Установите `zNear` на 23, и
+вы увидите, как передняя часть вращающихся кубов обрезается. Установите `zFar` на 24, и вы увидите, как задняя часть кубов
+обрезается.
+
+Остается только одна проблема. Эта матрица предполагает, что есть зритель в 0,0,0, и
+она предполагает, что он смотрит в отрицательном направлении Z и что положительный Y вверх.
+Наши матрицы до этого момента делали вещи по-другому.
+
+Чтобы заставить это появиться, нам нужно переместить это внутрь усеченной пирамиды.
+Мы можем сделать это, переместив наш F. Мы рисовали в (45, 150, 0). Давайте переместим его в (-150, 0, -360)
+и давайте установим вращение на что-то, что заставляет его появиться правой стороной вверх.
+
+
+
+Теперь, чтобы использовать это, нам просто нужно заменить наш старый вызов m4.projection на вызов
+m4.perspective
+
+```
+ var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
+ var zNear = 1;
+ var zFar = 2000;
+ var matrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
+ matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
+ matrix = m4.xRotate(matrix, rotation[0]);
+ matrix = m4.yRotate(matrix, rotation[1]);
+ matrix = m4.zRotate(matrix, rotation[2]);
+ matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
+```
+
+И вот это.
+
+{{{example url="../webgl-3d-perspective-matrix.html" }}}
+
+Мы вернулись к простому умножению матрицы, и мы получаем как поле зрения, так и возможность выбрать наше Z пространство.
+Мы не закончили, но эта статья становится слишком длинной. Далее [камеры](webgl-3d-camera.html).
+
+
+
Почему мы переместили F так далеко в Z (-360)?
+
+
+В других примерах у нас был F в (45, 150, 0), но в последнем примере
+он был перемещен в (-150, 0, -360). Почему его нужно было переместить так далеко?
+
+
+
+
+Причина в том, что до этого последнего примера наша функция m4.projection
+делала проекцию из пикселей в clip space. Это означает, что область, которую мы
+отображали, представляла 400x300 пикселей. Использование 'пикселей' действительно не
+имеет смысла в 3D.
+
+
+
+
+Другими словами, если бы мы попытались нарисовать с F в 0,0,0 и не повернутым, мы бы получили это
+
+
+
+
+
+
+F имеет свой верхний левый передний угол в начале координат. Проекция
+смотрит в отрицательном направлении Z, но наш F построен в положительном Z. Проекция имеет
+положительный Y вверх, но наш F построен с положительным Z вниз.
+
+
+
+Наша новая проекция видит только то, что в синей усеченной пирамиде. С -zNear = 1 и с полем зрения 60 градусов,
+то при Z = -1 усеченная пирамида только 1.154 единицы высотой и 1.154 * aspect единиц шириной. При Z = -2000 (-zFar) она 2309 единиц высотой.
+Поскольку наш F размером 150 единиц, а вид может видеть только 1.154
+единицы, когда что-то находится на -zNear, нам нужно переместить это довольно далеко от начала координат, чтобы
+увидеть все это.
+
+
+
+Перемещение его на -360 единиц в Z перемещает его внутрь усеченной пирамиды. Мы также повернули его, чтобы он был правой стороной вверх.
+
+
+
не в масштабе
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-3d-textures.md b/webgl/lessons/ru/webgl-3d-textures.md
new file mode 100644
index 000000000..ebe81600b
--- /dev/null
+++ b/webgl/lessons/ru/webgl-3d-textures.md
@@ -0,0 +1,444 @@
+Title: WebGL2 Текстуры
+Description: Как работают текстуры в WebGL
+TOC: Текстуры
+
+Эта статья является продолжением серии статей о WebGL.
+Первая [началась с основ](webgl-fundamentals.html),
+а предыдущая была об [анимации](webgl-animation.html).
+
+Как мы применяем текстуры в WebGL? Вы, вероятно, могли бы вывести, как это сделать, читая
+[статьи об обработке изображений](webgl-image-processing.html),
+но, вероятно, будет легче понять, если мы рассмотрим это более подробно.
+
+Первое, что нам нужно сделать, это настроить наши шейдеры для использования текстур. Вот
+изменения в вершинном шейдере. Нам нужно передать координаты текстуры. В этом
+случае мы просто передаем их прямо в фрагментный шейдер.
+
+ #version 300 es
+ in vec4 a_position;
+ *in vec2 a_texcoord;
+
+ uniform mat4 u_matrix;
+
+ +// varying для передачи координат текстуры в фрагментный шейдер
+ +out vec2 v_texcoord;
+
+ void main() {
+ // Умножаем позицию на матрицу.
+ gl_Position = u_matrix * a_position;
+
+ + // Передаем texcoord в фрагментный шейдер.
+ + v_texcoord = a_texcoord;
+ }
+
+В фрагментном шейдере мы объявляем uniform sampler2D, который позволяет нам ссылаться
+на текстуру. Мы используем координаты текстуры, переданные из вершинного шейдера,
+и мы вызываем `texture`, чтобы найти цвет из этой текстуры.
+
+ #version 300 es
+ precision highp float;
+
+ // Передается из вершинного шейдера.
+ *in vec2 v_texcoord;
+
+ *// Текстура.
+ *uniform sampler2D u_texture;
+
+ out vec4 outColor;
+
+ void main() {
+ * outColor = texture(u_texture, v_texcoord);
+ }
+
+Нам нужно настроить координаты текстуры
+
+ // ищем, куда должны идти данные вершин.
+ var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
+ *var texcoordAttributeLocation = gl.getAttribLocation(program, "a_texcoord");
+
+ ...
+
+ *// создаем буфер texcoord, делаем его текущим ARRAY_BUFFER
+ *// и копируем значения texcoord
+ *var texcoordBuffer = gl.createBuffer();
+ *gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
+ *setTexcoords(gl);
+ *
+ *// Включаем атрибут
+ *gl.enableVertexAttribArray(texcoordAttributeLocation);
+ *
+ *// Говорим атрибуту, как получать данные из texcoordBuffer (ARRAY_BUFFER)
+ *var size = 2; // 2 компонента на итерацию
+ *var type = gl.FLOAT; // данные - 32-битные значения с плавающей точкой
+ *var normalize = true; // конвертируем из 0-255 в 0.0-1.0
+ *var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) каждую итерацию, чтобы получить следующий texcoord
+ *var offset = 0; // начинаем с начала буфера
+ *gl.vertexAttribPointer(
+ * texcoordAttributeLocation, size, type, normalize, stride, offset);
+
+И вы можете видеть координаты, которые мы используем, которые отображают всю
+текстуру на каждый квадрат нашей 'F'.
+
+ *// Заполняем буфер координатами текстуры для F.
+ *function setTexcoords(gl) {
+ * gl.bufferData(
+ * gl.ARRAY_BUFFER,
+ * new Float32Array([
+ * // левая колонка спереди
+ * 0, 0,
+ * 0, 1,
+ * 1, 0,
+ * 0, 1,
+ * 1, 1,
+ * 1, 0,
+ *
+ * // верхняя перекладина спереди
+ * 0, 0,
+ * 0, 1,
+ * 1, 0,
+ * 0, 1,
+ * 1, 1,
+ * 1, 0,
+ * ...
+ * ]),
+ * gl.STATIC_DRAW);
+
+Нам также нужна текстура. Мы могли бы создать одну с нуля, но в этом случае давайте
+загрузим изображение, поскольку это, вероятно, самый распространенный способ.
+
+Вот изображение, которое мы собираемся использовать
+
+
+
+Какое захватывающее изображение! На самом деле изображение с 'F' на нем имеет четкое направление,
+поэтому легко сказать, повернуто оно или перевернуто и т.д., когда мы используем его как текстуру.
+
+Дело в загрузке изображения в том, что это происходит асинхронно. Мы запрашиваем изображение
+для загрузки, но браузеру требуется время, чтобы скачать его. Есть обычно
+2 решения для этого. Мы могли бы заставить код ждать, пока текстура не скачается,
+и только тогда начать рисовать. Другое решение - создать какую-то текстуру для использования,
+пока изображение скачивается. Таким образом, мы можем начать рендеринг немедленно. Затем, как только
+изображение было скачано, мы копируем изображение в текстуру. Мы будем использовать этот метод ниже.
+
+ *// Создаем текстуру.
+ *var 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]));
+ *
+ *// Асинхронно загружаем изображение
+ *var image = new Image();
+ *image.src = "resources/f-texture.png";
+ *image.addEventListener('load', function() {
+ * // Теперь, когда изображение загружено, копируем его в текстуру.
+ * gl.bindTexture(gl.TEXTURE_2D, texture);
+ * gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image);
+ * gl.generateMipmap(gl.TEXTURE_2D);
+ *});
+
+И вот это
+
+{{{example url="../webgl-3d-textures.html" }}}
+
+Что, если бы мы хотели использовать только часть текстуры на передней части 'F'? Текстуры ссылаются
+с "координатами текстуры", и координаты текстуры идут от 0.0 до 1.0 слева
+направо по текстуре и от 0.0 до 1.0 от первого пикселя на первой строке до последнего пикселя на последней строке.
+Обратите внимание, я не сказал верх или низ. Верх и низ не имеют смысла в пространстве текстуры,
+потому что пока вы не нарисуете что-то и не сориентируете это, нет верха и низа. Важно то, что вы
+предоставляете данные текстуры в WebGL. Начало этих данных начинается с координаты текстуры 0,0,
+и конец этих данных находится в 1,1
+
+
+
+Я загрузил текстуру в photoshop и посмотрел различные координаты в пикселях.
+
+
+
+Чтобы конвертировать из координат пикселей в координаты текстуры, мы можем просто использовать
+
+ texcoordX = pixelCoordX / (width - 1)
+ texcoordY = pixelCoordY / (height - 1)
+
+Вот координаты текстуры для передней части.
+
+ // левая колонка спереди
+ 38 / 255, 44 / 255,
+ 38 / 255, 223 / 255,
+ 113 / 255, 44 / 255,
+ 38 / 255, 223 / 255,
+ 113 / 255, 223 / 255,
+ 113 / 255, 44 / 255,
+
+ // верхняя перекладина спереди
+ 113 / 255, 44 / 255,
+ 113 / 255, 85 / 255,
+ 218 / 255, 44 / 255,
+ 113 / 255, 85 / 255,
+ 218 / 255, 85 / 255,
+ 218 / 255, 44 / 255,
+
+ // средняя перекладина спереди
+ 113 / 255, 112 / 255,
+ 113 / 255, 151 / 255,
+ 203 / 255, 112 / 255,
+ 113 / 255, 151 / 255,
+ 203 / 255, 151 / 255,
+ 203 / 255, 112 / 255,
+
+Я также использовал похожие координаты текстуры для задней части. И вот это.
+
+{{{example url="../webgl-3d-textures-texture-coords-mapped.html" }}}
+
+Не очень захватывающий дисплей, но, надеюсь, он демонстрирует, как использовать координаты текстуры. Если вы создаете
+геометрию в коде (кубы, сферы и т.д.), обычно довольно легко вычислить любые координаты текстуры, которые вы
+хотите. С другой стороны, если вы получаете 3D модели из программ 3D моделирования, таких как Blender, Maya, 3D Studio Max, то
+ваши художники (или вы) будут [настраивать координаты текстуры в этих пакетах, используя UV редактор](https://docs.blender.org/manual/en/3.4/modeling/meshes/uv/index.html).
+
+Так что происходит, если мы используем координаты текстуры вне диапазона 0.0 до 1.0. По умолчанию WebGL повторяет
+текстуру. 0.0 до 1.0 - это одна 'копия' текстуры. 1.0 до 2.0 - это другая копия. Даже -4.0 до -3.0 - это еще
+одна копия. Давайте отобразим плоскость, используя эти координаты текстуры.
+
+ -3, -1,
+ 2, -1,
+ -3, 4,
+ -3, 4,
+ 2, -1,
+ 2, 4,
+
+и вот это
+
+{{{example url="../webgl-3d-textures-repeat-clamp.html" }}}
+
+Вы можете сказать WebGL не повторять текстуру в определенном направлении, используя `CLAMP_TO_EDGE`. Например
+
+ 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);
+
+вы также можете сказать WebGL отражать текстуру, когда она повторяется, используя `gl.MIRRORED_REPEAT`.
+Нажмите кнопки в примере выше, чтобы увидеть разницу.
+
+Вы, возможно, заметили вызов `gl.generateMipmap` еще когда мы загружали текстуру. Для чего это?
+
+Представьте, у нас была эта текстура 16x16 пикселей.
+
+
+
+Теперь представьте, мы попытались нарисовать эту текстуру на полигоне размером 2x2 пикселя на экране. Какие цвета мы должны
+сделать для этих 4 пикселей? Есть 256 пикселей на выбор. В Photoshop, если бы вы масштабировали изображение 16x16 пикселей
+до 2x2, он бы усреднил 8x8 пикселей в каждом углу, чтобы сделать 4 пикселя в изображении 2x2. К сожалению,
+чтение 64 пикселей и усреднение их всех вместе было бы слишком медленно для GPU. На самом деле представьте, если бы у вас
+была текстура 2048x2084 пикселей, и вы попытались нарисовать ее 2x2 пикселя. Чтобы сделать то, что делает Photoshop для каждого из
+4 пикселей в результате 2x2, ему пришлось бы усреднить 1024x1024 пикселя или 1 миллион пикселей, умноженный на 4. Это слишком
+много, чтобы делать и все еще быть быстрым.
+
+Так что GPU использует мипмап. Мипмап - это коллекция прогрессивно меньших изображений,
+каждое из которых в 1/4 размера предыдущего. Мипмап для текстуры 16x16 выше выглядел бы примерно
+так.
+
+
+
+Обычно каждый меньший уровень - это просто билинейная интерполяция предыдущего уровня, и это
+то, что делает `gl.generateMipmap`. Он смотрит на самый большой уровень и генерирует все меньшие уровни для вас.
+Конечно, вы можете предоставить меньшие уровни сами, если хотите.
+
+Теперь, если вы попытаетесь нарисовать эту текстуру 16x16 пикселей только 2x2 пикселя на экране, WebGL может выбрать
+мип, который 2x2, который уже был усреднен из предыдущих мипов.
+
+Вы можете выбрать, что делает WebGL, установив фильтрацию текстуры для каждой текстуры. Есть 6 режимов
+
+* `NEAREST` = выбрать 1 пиксель из самого большого мипа
+* `LINEAR` = выбрать 4 пикселя из самого большого мипа и смешать их
+* `NEAREST_MIPMAP_NEAREST` = выбрать лучший мип, затем выбрать один пиксель из этого мипа
+* `LINEAR_MIPMAP_NEAREST` = выбрать лучший мип, затем смешать 4 пикселя из этого мипа
+* `NEAREST_MIPMAP_LINEAR` = выбрать лучшие 2 мипа, выбрать 1 пиксель из каждого, смешать их
+* `LINEAR_MIPMAP_LINEAR` = выбрать лучшие 2 мипа, выбрать 4 пикселя из каждого, смешать их
+
+Вы можете увидеть важность мипов в этих 2 примерах. Первый показывает, что если вы используете `NEAREST`
+или `LINEAR` и выбираете только из самого большого изображения, то вы получите много мерцания, потому что когда вещи
+двигаются, для каждого пикселя, который он рисует, ему приходится выбирать один пиксель из самого большого изображения. Это меняется в зависимости
+от размера и позиции, и поэтому иногда он выберет один пиксель, в другое время другой, и поэтому он
+мерцает.
+
+{{{example url="../webgl-3d-textures-mips.html" }}}
+
+Пример выше преувеличен, чтобы показать проблему.
+Обратите внимание, как сильно мерцают те, что слева и в середине, тогда как те, что справа, мерцают меньше.
+Те, что справа, также имеют смешанные цвета, поскольку они используют мипы. Чем меньше вы рисуете текстуру, тем дальше друг от друга WebGL будет
+выбирать пиксели. Вот почему, например, тот, что внизу посередине, даже несмотря на то, что он использует `LINEAR` и смешивает
+4 пикселя, мерцает разными цветами, потому что эти 4 пикселя из разных углов изображения 16x16 в зависимости от того, какие
+4 выбраны, вы получите другой цвет. Тот, что внизу справа, хотя остается постоянного цвета,
+потому что он использует второй по величине мип.
+
+Этот второй пример показывает полигоны, которые уходят глубоко вдаль.
+
+{{{example url="../webgl-3d-textures-mips-tri-linear.html" }}}
+
+6 лучей, идущих в экран, используют 6 режимов фильтрации, перечисленных выше. Луч вверху слева использует `NEAREST`,
+и вы можете видеть, что он явно очень блочный. Тот, что вверху посередине, использует `LINEAR`, и он не намного лучше.
+Тот, что вверху справа, использует `NEAREST_MIPMAP_NEAREST`. Нажмите на изображение, чтобы переключиться на текстуру, где каждый мип
+разного цвета, и вы легко увидите, где он выбирает использовать конкретный мип. Тот, что внизу слева, использует
+`LINEAR_MIPMAP_NEAREST`, что означает, что он выбирает лучший мип, а затем смешивает 4 пикселя в этом мипе. Вы все еще можете видеть
+четкую область, где он переключается с одного мипа на следующий мип. Тот, что внизу посередине, использует `NEAREST_MIPMAP_LINEAR`,
+что означает выбор лучших 2 мипов, выбор одного пикселя из каждого и смешивание
+их. Если вы посмотрите внимательно, вы можете увидеть, как он все еще блочный, особенно в горизонтальном направлении.
+Тот, что внизу справа, использует `LINEAR_MIPMAP_LINEAR`, который выбирает лучшие 2 мипа, выбирает 4 пикселя из каждого,
+и смешивает все 8 пикселей.
+
+
+
мипы разного цвета
+
+Вы можете думать, зачем вам когда-либо выбирать что-то другое, кроме `LINEAR_MIPMAP_LINEAR`, который, возможно,
+лучший. Есть много причин. Одна в том, что `LINEAR_MIPMAP_LINEAR` самый медленный. Чтение 8 пикселей
+медленнее, чем чтение 1 пикселя. На современном GPU оборудовании это, вероятно, не проблема, если вы используете только 1
+текстуру за раз, но современные игры могут использовать 2-4 текстуры одновременно. 4 текстуры * 8 пикселей на текстуру =
+необходимость читать 32 пикселя для каждого нарисованного пикселя. Это будет медленно. Другая причина в том, что если вы пытаетесь
+достичь определенного эффекта. Например, если вы хотите, чтобы что-то имело этот пикселизированный *ретро* вид, возможно, вы
+хотите использовать `NEAREST`. Мипы также занимают память. На самом деле они занимают на 33% больше памяти. Это может быть много памяти,
+особенно для очень большой текстуры, как вы могли бы использовать на титульном экране игры. Если вы никогда не собираетесь
+рисовать что-то меньше, чем самый большой мип, зачем тратить память на меньшие мипы. Вместо этого просто используйте `NEAREST`
+или `LINEAR`, поскольку они используют только первый мип.
+
+Чтобы установить фильтрацию, вы вызываете `gl.texParameter` так
+
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+
+`TEXTURE_MIN_FILTER` - это настройка, используемая, когда размер, который вы рисуете, меньше, чем самый большой мип.
+`TEXTURE_MAG_FILTER` - это настройка, используемая, когда размер, который вы рисуете, больше, чем самый большой мип. Для
+`TEXTURE_MAG_FILTER` только `NEAREST` и `LINEAR` являются валидными настройками.
+
+Что нужно знать, WebGL2 требует, чтобы текстуры были "texture complete", иначе они не будут рендериться.
+"texture complete" означает, что либо
+
+1. Вы установили фильтрацию так, чтобы она использовала только первый уровень мипа, что означает
+ установку `TEXTURE_MIN_FILTER` либо в `LINEAR`, либо в `NEAREST`.
+
+2. Если вы используете мипы, то они должны быть правильных размеров, и вы должны предоставить ВСЕ ИЗ НИХ
+ вплоть до размера 1x1.
+
+Самый простой способ сделать это - вызвать `gl.generateMipmap`. В противном случае, если вы предоставляете свои собственные мипы, вам нужно предоставить
+все из них, или вы получите ошибку.
+
+Общий вопрос: "Как я могу применить другое изображение к каждой грани куба?". Например, скажем, у нас
+были эти 6 изображений.
+
+
+
+
+
+
+
+
+
+
+3 ответа приходят на ум
+
+1. сделать сложный шейдер, который ссылается на 6 текстур, и передать какую-то дополнительную информацию на вершину в
+вершинный шейдер, которая передается в фрагментный шейдер, чтобы решить, какую текстуру использовать. НЕ ДЕЛАЙТЕ ЭТОГО!
+Немного размышлений сделало бы ясным, что вам пришлось бы написать тонны разных шейдеров, если бы вы
+хотели сделать то же самое для разных форм с большим количеством сторон и т.д.
+
+2. нарисовать 6 плоскостей вместо куба. Это общее решение. Это не плохо, но это также работает только
+для маленьких форм, как куб. Если бы у вас была сфера с 1000 квадратов, и вы хотели положить другую текстуру
+на каждый квадрат, вам пришлось бы нарисовать 1000 плоскостей, и это было бы медленно.
+
+3. Решение, осмелюсь сказать, *лучшее* - это положить все изображения в 1 текстуру и использовать координаты текстуры,
+чтобы отобразить другую часть текстуры на каждую грань куба. Это техника, которую используют практически
+все высокопроизводительные приложения (читай *игры*). Так, например, мы бы положили все изображения в одну текстуру, возможно,
+так
+
+
+
+и затем использовать другой набор координат текстуры для каждой грани куба.
+
+ // выбираем изображение вверху слева
+ 0 , 0 ,
+ 0 , 0.5,
+ 0.25, 0 ,
+ 0 , 0.5,
+ 0.25, 0.5,
+ 0.25, 0 ,
+ // выбираем изображение вверху посередине
+ 0.25, 0 ,
+ 0.5 , 0 ,
+ 0.25, 0.5,
+ 0.25, 0.5,
+ 0.5 , 0 ,
+ 0.5 , 0.5,
+ // выбираем изображение вверху справа
+ 0.5 , 0 ,
+ 0.5 , 0.5,
+ 0.75, 0 ,
+ 0.5 , 0.5,
+ 0.75, 0.5,
+ 0.75, 0 ,
+ // выбираем изображение внизу слева
+ 0 , 0.5,
+ 0.25, 0.5,
+ 0 , 1 ,
+ 0 , 1 ,
+ 0.25, 0.5,
+ 0.25, 1 ,
+ // выбираем изображение внизу посередине
+ 0.25, 0.5,
+ 0.25, 1 ,
+ 0.5 , 0.5,
+ 0.25, 1 ,
+ 0.5 , 1 ,
+ 0.5 , 0.5,
+ // выбираем изображение внизу справа
+ 0.5 , 0.5,
+ 0.75, 0.5,
+ 0.5 , 1 ,
+ 0.5 , 1 ,
+ 0.75, 0.5,
+ 0.75, 1 ,
+
+И мы получаем
+
+{{{example url="../webgl-3d-textures-texture-atlas.html" }}}
+
+Этот стиль применения нескольких изображений, используя 1 текстуру, часто называется [*texture atlas*](https://www.google.com/?ion=1&espv=2#q=texture%20atlas).
+Это лучше всего, потому что есть только 1 текстура для загрузки, шейдер остается простым, поскольку ему нужно ссылаться только на 1 текстуру, и это требует
+только 1 вызов отрисовки для рисования формы вместо 1 вызова отрисовки на текстуру, как это могло бы быть, если бы мы разделили это на
+плоскости.
+
+Несколько других очень важных вещей, которые вы, возможно, захотите знать о текстурах.
+Одна - [как работает состояние текстурного блока](webgl-texture-units.html).
+Одна - [как использовать 2 или более текстур одновременно](webgl-2-textures.html). Другая
+- [как использовать изображения с других доменов](webgl-cors-permission.html).
+
+Далее [давайте начнем упрощать с меньшим количеством кода, больше веселья](webgl-less-code-more-fun.html).
+
+
+
UVs vs. Координаты текстуры
+
Координаты текстуры часто сокращаются до texture coords, texcoords или UVs
+(произносится Ew-Vees). Я не имею представления, откуда пришел термин UVs, кроме того, что
+позиции вершин часто используют x, y, z, w, поэтому для координат текстуры они решили использовать
+s, t, u, v, чтобы попытаться сделать ясным, к какому из 2 типов вы обращаетесь.
+Учитывая это, вы бы подумали, что они назывались бы Es-Tees, и на самом деле, если вы посмотрите
+на настройки обертывания текстуры, они называются TEXTURE_WRAP_S и
+TEXTURE_WRAP_T, но по какой-то причине, пока я работаю
+в графике, люди называли их Ew-Vees.
+
+
Так что теперь вы знаете, если кто-то говорит UVs, они говорят о координатах текстуры.
+
+
+
+
Изображения не степени 2
+
Если вы привыкли к WebGL1, WebGL1 имел ограничение, что текстуры с размерами,
+которые не были степенью 2, другими словами **не** 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 и т.д.,
+не могли использовать мипы и не могли повторяться. В WebGL2 эти ограничения исчезли.
+УРА!
+
+
\ No newline at end of file
diff --git a/webgl/lessons/ru/webgl-and-alpha.md b/webgl/lessons/ru/webgl-and-alpha.md
new file mode 100644
index 000000000..ce945d7eb
--- /dev/null
+++ b/webgl/lessons/ru/webgl-and-alpha.md
@@ -0,0 +1,126 @@
+Title: WebGL2 и альфа-канал
+Description: Как альфа-канал в WebGL отличается от альфа-канала в OpenGL
+TOC: WebGL2 и альфа-канал
+
+
+Я заметил, что некоторые разработчики OpenGL испытывают проблемы с тем, как WebGL
+обрабатывает альфа-канал в буфере кадров (т.е. в canvas), поэтому я подумал, что
+было бы полезно рассмотреть некоторые различия между WebGL и OpenGL, связанные с альфа-каналом.
+
+Самое большое различие между OpenGL и WebGL заключается в том, что OpenGL
+рендерит в буфер кадров, который не композируется ни с чем,
+или эффективно не композируется с чем-либо оконным менеджером ОС,
+поэтому не важно, какое у вас значение альфа.
+
+WebGL композируется браузером с веб-страницей, и
+по умолчанию используется предварительно умноженный альфа-канал, так же как в тегах ``
+с прозрачностью и 2D canvas тегах.
+
+WebGL имеет несколько способов сделать это более похожим на OpenGL.
+
+### #1) Скажите WebGL, что вы хотите композицию с непредварительно умноженным альфа-каналом
+
+ gl = canvas.getContext("webgl2", {
+ premultipliedAlpha: false // Запросить непредварительно умноженный альфа-канал
+ });
+
+По умолчанию это true.
+
+Конечно, результат все равно будет композироваться со страницей с любым
+цветом фона, который окажется под canvas (цвет фона canvas, цвет фона
+контейнера canvas, цвет фона страницы, содержимое за canvas, если у canvas z-index > 0, и т.д....)
+другими словами, цвет, который CSS определяет для этой области веб-страницы.
+
+Очень хороший способ найти проблемы с альфа-каналом - установить
+фон canvas ярким цветом, например красным. Вы сразу увидите,
+что происходит.
+
+