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, возможно, вам будет интересно посмотреть эти статьи + + + +# Содержание + +{{{include "webgl/lessons/ru/toc.html"}}} + + + \ No newline at end of file diff --git a/webgl/lessons/ru/langinfo.hanson b/webgl/lessons/ru/langinfo.hanson new file mode 100644 index 000000000..2fae23f52 --- /dev/null +++ b/webgl/lessons/ru/langinfo.hanson @@ -0,0 +1,35 @@ +{ + language: 'Русский', + langCode: 'ru', + defaultExampleCaption: "Нажмите, чтобы открыть в новом окне", + title: 'WebGL2 Основы', + description: 'Изучайте WebGL с основ теории. Это не сложно!', + link: 'https://webgl2fundamentals.org/webgl/lessons/ru', + commentSectionHeader: ` +
Есть предложения или замечания? Создайте issue на GitHub.
+ `, + missing: "Русский перевод пока не готов ( ̄△ ̄)~[Помогите с переводом](https://github.com/gfxfundamentals/webgl2-fundamentals)! \n\n[Оригинал на английском]({{{origLink}}}).", + contribTemplate: 'Спасибо ${login}
за ${contributions} вкладов', + toc: 'Содержание', + categoryMapping: { + 'fundamentals': "Основы", + 'webgl2': "WebGL2 vs WebGL1", + 'image-processing': "Обработка изображений", + 'matrices': "2D трансформации, повороты, масштабирование и матричные операции", + '3d': "3D", + 'lighting': "Освещение", + 'organization': "Организация и рефакторинг", + 'geometry': "Геометрия", + 'textures': "Текстуры", + 'rendertargets': "Рендеринг в текстуру", + 'shadows': "Тени", + 'techniques': "Техники", + '2d': "2D", + 'text': "Текст", + 'gpgpu': "GPGPU", + 'tips': "Советы", + 'misc': "Разное", + 'reference': "Справочник", + 'optimization': "Оптимизация", + }, +} \ No newline at end of file diff --git a/webgl/lessons/ru/toc.html b/webgl/lessons/ru/toc.html new file mode 100644 index 000000000..1815cfba1 --- /dev/null +++ b/webgl/lessons/ru/toc.html @@ -0,0 +1,6 @@ +{{{tocHtml}}} + \ 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 - это всё о творческом использовании функций, +которые он предоставляет.

+

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

+
{{#escapehtml}}
+#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 * a_position).xy;
+}
+{{/escapehtml}}
+

Теперь мы можем удалить код, который настраивал координаты текстуры, и он будет +работать точно так же, как раньше.

+{{{example url="../webgl-2d-drawimage-08.html" }}} +
\ 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). + +
+

Математика vs Программирование vs WebGL

+

+Прежде чем мы начнем, если вы ранее изучали линейную алгебру или в целом +имеете опыт работы с матрицами, то +пожалуйста, прочитайте эту статью перед +продолжением ниже.. +

+

+Если у вас мало или нет опыта с матрицами, то смело +пропустите ссылку выше пока и продолжайте чтение. +

+
+ +В последних 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.02.03.0
4.05.06.0
7.08.09.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.00.00.0
0.01.00.0
txty1.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). + +Аналогично давайте сделаем поворот. Как мы указали в посте о повороте, +нам просто нужны синус и косинус угла, на который мы хотим повернуть, поэтому + +
+s = Math.sin(angleToRotateInRadians);
+c = Math.cos(angleToRotateInRadians);
+
+ +И мы строим матрицу так + +
c-s0.0
sc0.0
0.00.01.0
+ +Применяя матрицу, мы получаем это + +
++ + +
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 
+ +Зачеркивая все умножения на 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 + +И мы строим матрицу так + +
sx0.00.0
0.0sy0.0
0.00.01.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" }}} + +Почему это работает? Ну, посмотрите на математику. + +
+    rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
+    rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;
+
+ +Допустим, у вас есть прямоугольник, и вы хотите его вращать. +Прежде чем вы начнете его вращать, верхний правый угол находится в точке 3.0, 9.0. +Давайте выберем точку на единичной окружности на 30 градусов по часовой стрелке от 12 часов. + + + +Позиция на окружности там 0.50 и 0.87 + +
+   3.0 * 0.87 + 9.0 * 0.50 = 7.1
+   9.0 * 0.87 - 3.0 * 0.50 = 6.3
+
+ +Это именно там, где нам нужно + + + +То же самое для 60 градусов по часовой стрелке + + + +Позиция на окружности там 0.87 и 0.50 + +
+   3.0 * 0.50 + 9.0 * 0.87 = 9.3
+   9.0 * 0.50 - 3.0 * 0.87 = 1.9
+
+ +Вы можете видеть, что когда мы вращаем эту точку по часовой стрелке вправо, значение 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 источниками света. + +
{{#escapehtml}}
+  // Настройка
+  glViewport(0, 0, width, height);
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  gluPerspective(70.0, width / height, 1, 1000);
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+
+  glClearColor(0.0, 0.0, 0.0, 0.0);
+  glEnable(GL_DEPTH_TEST);
+  glShadeModel(GL_SMOOTH);
+  glEnable(GL_LIGHTING);
+
+  // Настройка 2 источников света
+  glEnable(GL_LIGHT0);
+  glEnable(GL_LIGHT1);
+  float light0_position[] = {  200, 100, 300, };
+  float light1_position[] = { -200, 100, 300, };
+  float light0_color[] = { 1, 0, 0.25, 1, };
+  float light1_color[] = { 0, 0.25, 1, 1, };
+  glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_color);
+  glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_color);
+  glLightfv(GL_LIGHT0, GL_POSITION, light0_position);
+  glLightfv(GL_LIGHT1, GL_POSITION, light1_position);
+...
+
+  // Рисуем куб.
+  static int count = 0;
+  ++count;
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+  glLoadIdentity();
+  double angle = count * 0.1;
+  glTranslatef(0, 0, -400);
+  glRotatef(angle, 0, 1, 0);
+
+  glBegin(GL_TRIANGLES);
+  glNormal3f(0, 0, 1);
+  glVertex3f(-100, -100, 100);
+  glVertex3f( 100, -100, 100);
+  glVertex3f(-100,  100, 100);
+  glVertex3f(-100,  100, 100);
+  glVertex3f( 100, -100, 100);
+  glVertex3f( 100,  100, 100);
+
+  /*
+  ...
+  ... повторить для ещё 5 граней куба
+  ...
+  */
+
+  glEnd();
+{{/escapehtml}}
+ +Обратите внимание, как нам почти не нужно знание 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` часть матрицы. + +
++----+----+----+----+
+|    |    |    |    |
++----+----+----+----+
+|    |    |    |    |
++----+----+----+----+
+| Zx | Zy | Zz |    |
++----+----+----+----+
+|    |    |    |    |
++----+----+----+----+
+
+ +Эта часть матрицы представляет ось 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 возвращает матрицу, которая ориентирует +и позиционирует объект в мировом пространстве, по моему мнению. +

+{{{example url="../webgl-3d-camera-look-at-heads.html" }}} +
\ 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 работает, это не обычный способ. Вместо этого кто-то подставил +всю математику и упростил её до формулы, подобной этой + +
+
+invT = (1 - t)
+P = P1 * invT^3 +
+    P2 * 3 * t * invT^2 +
+    P3 * 3 * invT * t^2 +
+    P4 * t^3
+
+
+ +Где `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, вы увидите этот модуло +при вычислении угла.

+
+for (let division = 0; division <= numDivisions; ++division) {
+  const u = division / numDivisions;
+*  const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
+
+

Почему он там?

+

Когда мы вращаем точки полностью вокруг круга, мы действительно хотим, чтобы первая +и последняя точки совпадали. 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), чтобы сделать поиск совпадающих точек +быстрее, но это кажется слишком много для этой статьи. +

+

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

+
+  const angle = lerp(startAngle, endAngle, u);
+
+А новый код таким +
+  const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
+
+

Из-за модуло angle, когда endAngle равен Math.PI * 2, становится 0 +и поэтому он такой же, как начало. Шов исчез. Проблема решена!

+

Тем не менее, даже с изменением, если вы установите distance на 0.001 +и divisions на 60, это занимает почти секунду на моей машине для пересчета сетки. Хотя +могут быть способы оптимизировать это, я думаю, что суть в понимании, что генерация сложных +сеток - это вообще медленная операция. Это всего лишь один пример того, почему 3D игра может работать на 60fps, +но 3D пакет моделирования часто работает на очень медленных частотах кадров. +

+
+ +
+

Не является ли матричная математика избыточной здесь?

+

Когда мы вытачиваем точки, есть этот код для вращения.

+
+const mat = m4.yRotation(angle);
+...
+points.forEach((p, ndx) => {
+  const tp = m4.transformPoint(mat, [...p, 0]);
+  ...
+
+

Преобразование произвольной 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). + +
+

Альтернативы mat3(u_worldInverseTranspose) * a_normal

+

В нашем шейдере выше есть строка, как эта

+
+v_normal = mat3(u_worldInverseTranspose) * a_normal;
+
+

Мы могли бы сделать это

+
+v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
+
+

Потому что мы установили w в 0 перед умножением, это +привело бы к умножению трансляции из матрицы на 0, эффективно удаляя ее. Я думаю, что это +более распространенный способ сделать это. Способ mat3 выглядел чище для меня, но +я часто делал это и так тоже.

+

Еще одно решение было бы сделать u_worldInverseTranspose mat3. +Есть 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.

+ + +
genType clamp (genType x, genType minVal, genType maxVal)
+genType clamp (genType x, float minVal, float maxVal)

+Возвращает min (max (x, minVal), maxVal). +Результаты не определены, если minVal > maxVal

+ + + +
genType smoothstep (genType edge0, genType edge1, genType x)
+genType smoothstep (float edge0, float edge1, genType x)

+Возвращает 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, она была бы + +
+10 / 2 = 5
+20 / 2 = 10
+abs(5 - 10) = 5
+
+ +длиной 5 пикселей. При Z = 3 она была бы + +
+10 / 3 = 3.333
+20 / 3 = 6.666
+abs(3.333 - 6.666) = 3.333
+
+ +Вы можете видеть, что по мере увеличения 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. + +Матрица, как эта + +
+1, 0, 0, 0,
+0, 1, 0, 0,
+0, 0, 1, 1,
+0, 0, 0, 0,
+
+ +скопирует z в w. Вы можете смотреть на каждый из этих столбцов как + +
+x_out = x_in * 1 +
+        y_in * 0 +
+        z_in * 0 +
+        w_in * 0 ;
+
+y_out = x_in * 0 +
+        y_in * 1 +
+        z_in * 0 +
+        w_in * 0 ;
+
+z_out = x_in * 0 +
+        y_in * 0 +
+        z_in * 1 +
+        w_in * 0 ;
+
+w_out = x_in * 0 +
+        y_in * 0 +
+        z_in * 1 +
+        w_in * 0 ;
+
+ +что при упрощении есть + +
+x_out = x_in;
+y_out = y_in;
+z_out = z_in;
+w_out = z_in;
+
+ +Мы можем добавить плюс 1, который у нас был раньше, с этой матрицей, поскольку мы знаем, что `w_in` всегда 1.0. + +
+1, 0, 0, 0,
+0, 1, 0, 0,
+0, 0, 1, 1,
+0, 0, 0, 1,
+
+ +это изменит вычисление W на + +
+w_out = x_in * 0 +
+        y_in * 0 +
+        z_in * 1 +
+        w_in * 1 ;
+
+ +и поскольку мы знаем, что `w_in` = 1.0, то это действительно + +
+w_out = z_in + 1;
+
+ +Наконец, мы можем вернуть наш fudgeFactor, если матрица такая + +
+1, 0, 0, 0,
+0, 1, 0, 0,
+0, 0, 1, fudgeFactor,
+0, 0, 0, 1,
+
+ +что означает + +
+w_out = x_in * 0 +
+        y_in * 0 +
+        z_in * fudgeFactor +
+        w_in * 1 ;
+
+ +и упрощенно это + +
+w_out = z_in * fudgeFactor + 1;
+
+ +Итак, давайте снова изменим программу, чтобы просто использовать матрицы. + +Сначала давайте вернем вершинный шейдер. Он снова простой + +``` +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 ярким цветом, например красным. Вы сразу увидите, +что происходит. + + + +Вы также можете установить его черным, что скроет любые проблемы с альфа-каналом. + +### #2) Скажите WebGL, что вы не хотите альфа-канал в буфере кадров + + gl = canvas.getContext("webgl", { alpha: false }}; + +Это сделает его более похожим на OpenGL, поскольку буфер кадров будет содержать только +RGB. Это, вероятно, лучший вариант, потому что хороший браузер может увидеть, что +у вас нет альфа-канала, и фактически оптимизировать способ композиции WebGL. Конечно, +это также означает, что в буфере кадров действительно не будет альфа-канала, поэтому если вы +используете альфа-канал в буфере кадров для какой-то цели, это может не сработать для вас. +Мало приложений, которые я знаю, используют альфа-канал в буфере кадров. Я думаю, что это +должно было быть по умолчанию. + +### #3) Очистите альфа-канал в конце рендеринга + + .. + renderScene(); + .. + // Установите альфа-канал буфера кадров в 1.0, установив + // цвет очистки в 1 + gl.clearColor(1, 1, 1, 1); + + // Сказать WebGL воздействовать только на альфа-канал + gl.colorMask(false, false, false, true); + + // очистить + gl.clear(gl.COLOR_BUFFER_BIT); + +Очистка обычно очень быстрая, так как в большинстве оборудования есть специальный случай для этого. +Я делал это во многих своих первых WebGL демо. Если бы я был умным, я бы переключился на +метод #2 выше. Может быть, я сделаю это сразу после публикации этого. Кажется, что +большинство WebGL библиотек должны по умолчанию использовать этот метод. Те немногие разработчики, +которые действительно используют альфа-канал для эффектов композиции, могут запросить его. Остальные +просто получат лучшую производительность и меньше сюрпризов. + +### #4) Очистите альфа-канал один раз, затем не рендерите в него больше + + // Во время инициализации. Очистите буфер кадров. + gl.clearColor(1,1,1,1); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Отключите рендеринг в альфа-канал + gl.colorMask(true, true, true, false); + +Конечно, если вы рендерите в свои собственные framebuffer'ы, вам может понадобиться включить +рендеринг в альфа-канал обратно, а затем отключить его снова, когда вы переключитесь на +рендеринг в canvas. + +### #5) Обработка изображений + +По умолчанию, если вы загружаете изображения с альфа-каналом в WebGL, WebGL будет +предоставлять значения такими, как они есть в файле, с цветовыми значениями, не +предварительно умноженными. Это обычно то, к чему я привык для программ OpenGL, +потому что это без потерь, тогда как предварительно умноженный - с потерями. + + 1, 0.5, 0.5, 0 // RGBA + +Это возможное непредварительно умноженное значение, тогда как предварительно умноженное - это +невозможное значение, потому что `a = 0`, что означает, что `r`, `g`, и `b` должны +быть нулевыми. + +При загрузке изображения вы можете заставить WebGL предварительно умножить альфа-канал, если хотите. +Вы делаете это, устанавливая `UNPACK_PREMULTIPLY_ALPHA_WEBGL` в true, как здесь: + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + +По умолчанию это непредварительно умноженный. + +Имейте в виду, что большинство, если не все, реализации Canvas 2D работают с +предварительно умноженным альфа-каналом. Это означает, что когда вы передаете их в WebGL и +`UNPACK_PREMULTIPLY_ALPHA_WEBGL` равно false, WebGL преобразует их обратно в непредварительно умноженные. + +### #6) Использование уравнения смешивания, которое работает с предварительно умноженным альфа-каналом. + +Почти все приложения OpenGL, которые я писал или над которыми работал, используют + + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +Это работает для текстур с непредварительно умноженным альфа-каналом. + +Если вы действительно хотите работать с текстурами с предварительно умноженным альфа-каналом, то +вероятно вам нужно + + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + +Это методы, о которых я знаю. Если вы знаете больше, пожалуйста, опубликуйте их ниже. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-animation.md b/webgl/lessons/ru/webgl-animation.md new file mode 100644 index 000000000..576f73a09 --- /dev/null +++ b/webgl/lessons/ru/webgl-animation.md @@ -0,0 +1,137 @@ +Title: WebGL2 - Анимация +Description: Как делать анимацию в WebGL +TOC: Анимация + + +Этот пост является продолжением серии постов о WebGL. +Первый [начался с основ](webgl-fundamentals.html). +и предыдущий был о [3D камерах](webgl-3d-camera.html). +Если вы не читали их, пожалуйста, просмотрите их сначала. + +Как мы анимируем что-то в WebGL? + +На самом деле это не специфично для WebGL, но вообще, если вы хотите +анимировать что-то в JavaScript, вам нужно изменить что-то +со временем и нарисовать снова. + +Мы можем взять один из наших предыдущих примеров и анимировать его следующим образом. + + *var fieldOfViewRadians = degToRad(60); + *var rotationSpeed = 1.2; + + *requestAnimationFrame(drawScene); + + // Рисуем сцену. + function drawScene() { + * // Каждый кадр увеличиваем вращение немного. + * rotation[1] += rotationSpeed / 60.0; + + ... + * // Вызываем drawScene снова в следующем кадре + * requestAnimationFrame(drawScene); + } + +И вот это + +{{{example url="../webgl-animation-not-frame-rate-independent.html" }}} + +Есть тонкая проблема, однако. Код выше имеет +`rotationSpeed / 60.0`. Мы разделили на 60.0, потому что предположили, что браузер +будет отвечать на requestAnimationFrame 60 раз в секунду, что довольно распространено. + +Это на самом деле не валидное предположение, однако. Может быть, пользователь на маломощном +устройстве, как старый смартфон. Или может быть, пользователь запускает какую-то тяжелую программу в +фоне. Есть все виды причин, по которым браузер может не отображать +кадры со скоростью 60 кадров в секунду. Может быть, это 2020 год, и все машины работают на 240 +кадрах в секунду сейчас. Может быть, пользователь - геймер и имеет CRT монитор, работающий на 90 +кадрах в секунду. + +Вы можете увидеть проблему в этом примере + +{{{diagram url="../webgl-animation-frame-rate-issues.html" }}} + +В примере выше мы хотим вращать все 'F' с одинаковой скоростью. +'F' в середине работает на полной скорости и не зависит от частоты кадров. Тот, +что слева и справа, симулируют, если бы браузер работал только на 1/8 +максимальной скорости для текущей машины. Тот, что слева, **НЕ** зависит от частоты +кадров. Тот, что справа, **ЗАВИСИТ** от частоты кадров. + +Обратите внимание, что поскольку тот, что слева, не учитывает, что частота кадров +может быть медленной, он не поспевает. Тот, что справа, однако, даже though он +работает на 1/8 частоты кадров, он поспевает за тем, что в середине, работающим на полной +скорости. + +Способ сделать анимацию независимой от частоты кадров - это вычислить, сколько времени потребовалось +между кадрами, и использовать это для вычисления, сколько анимировать в этом кадре. + +Сначала нам нужно получить время. К счастью, `requestAnimationFrame` передает +нам время с момента загрузки страницы, когда он вызывает нас. + +Я нахожу это легче всего, если мы получаем время в секундах, но поскольку `requestAnimationFrame` +передает нам время в миллисекундах (тысячных долях секунды), нам нужно умножить на 0.001, +чтобы получить секунды. + +Итак, мы можем затем вычислить дельта-время так + + *var then = 0; + + requestAnimationFrame(drawScene); + + // Рисуем сцену. + *function drawScene(now) { + * // Преобразуем время в секунды + * now *= 0.001; + * // Вычитаем предыдущее время из текущего времени + * var deltaTime = now - then; + * // Запоминаем текущее время для следующего кадра. + * then = now; + + ... + +Как только у нас есть `deltaTime` в секундах, тогда все наши вычисления могут быть в том, сколько +единиц в секунду мы хотим, чтобы что-то происходило. В этом случае +`rotationSpeed` равен 1.2, что означает, что мы хотим вращать 1.2 радиана в секунду. +Это примерно 1/5 оборота, или другими словами, потребуется около 5 секунд, чтобы +обернуться полностью, независимо от частоты кадров. + + * rotation[1] += rotationSpeed * deltaTime; + +Вот это работает. + +{{{example url="../webgl-animation.html" }}} + +Вы вряд ли увидите разницу с тем, +что вверху этой страницы, если вы не на медленной машине, но если вы не +делаете ваши анимации независимыми от частоты кадров, у вас, вероятно, будут некоторые пользователи, +которые получают очень другой опыт, чем вы планировали. + +Далее [как применять текстуры](webgl-3d-textures.html). + +
+

Не используйте setInterval или setTimeout!

+

Если вы программировали анимацию в JavaScript в прошлом, +вы могли использовать либо setInterval, либо setTimeout, чтобы ваша +функция рисования вызывалась. +

+Проблемы с использованием setInterval или setTimeout для анимации +двукратны. Во-первых, и setInterval, и setTimeout не имеют отношения +к тому, что браузер что-то отображает. Они не синхронизированы с тем, когда браузер +собирается нарисовать новый кадр, и поэтому могут быть не синхронизированы с машиной пользователя. +Если вы используете setInterval или setTimeout и предполагаете 60 кадров +в секунду, а машина пользователя на самом деле работает на какой-то другой частоте кадров, вы +будете не синхронизированы с их машиной. +

+Другая проблема в том, что браузер не имеет представления, почему вы используете setInterval или +setTimeout. Так, например, даже когда ваша страница не видна, +как когда она не является передней вкладкой, браузер все еще должен выполнять ваш код. +Может быть, вы используете setTimeout или setInterval для проверки +новой почты или твитов. Нет способа для браузера знать. Это нормально, если +вы просто проверяете каждые несколько секунд новые сообщения, но это не нормально, если +вы пытаетесь нарисовать 1000 объектов в WebGL. Вы будете эффективно DOS'ить +машину пользователя своей невидимой вкладкой, рисуя вещи, которые они даже не могут видеть. +

+requestAnimationFrame решает обе эти проблемы. Он вызывает вас как раз в +правильное время, чтобы синхронизировать вашу анимацию с экраном, и он также вызывает вас только если +ваша вкладка видна. +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-anti-patterns.md b/webgl/lessons/ru/webgl-anti-patterns.md new file mode 100644 index 000000000..9a2f1202f --- /dev/null +++ b/webgl/lessons/ru/webgl-anti-patterns.md @@ -0,0 +1,306 @@ +Title: WebGL2 Анти-паттерны +Description: Что не следует делать в WebGL, почему не следует и что делать вместо этого +TOC: Анти-паттерны + + +Это список анти-паттернов для WebGL. Анти-паттерны - это вещи, которых следует избегать + +1. Добавление `viewportWidth` и `viewportHeight` в `WebGLRenderingContext` + + Некоторые коды добавляют свойства для ширины и высоты viewport + и прикрепляют их к `WebGLRenderingContext` примерно так: + + gl = canvas.getContext("webgl2"); + gl.viewportWidth = canvas.width; // ПЛОХО!!! + gl.viewportHeight = canvas.height; // ПЛОХО!!! + + Затем они могут делать что-то вроде этого: + + gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); + + **Почему это плохо:** + + Это объективно плохо, потому что теперь у вас есть 2 свойства, которые нужно обновлять + каждый раз, когда вы изменяете размер canvas. Например, если вы измените размер + canvas, когда пользователь изменяет размер окна, `gl.viewportWidth` и `gl.viewportHeight` + будут неправильными, если вы не установите их снова. + + Это субъективно плохо, потому что любой новый WebGL программист взглянет на ваш код + и, вероятно, подумает, что `gl.viewportWidth` и `gl.viewportHeight` являются частью спецификации WebGL, + что будет путать их месяцами. + + **Что делать вместо этого:** + + Зачем создавать себе больше работы? WebGL контекст имеет доступ к своему canvas, + и у него есть размер. + +
+    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+    
+ + Контекст также имеет свою ширину и высоту прямо на нем. + + // Когда вам нужно установить viewport в соответствии с размером drawingBuffer canvas, + // это всегда будет правильно + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + + Еще лучше, это будет обрабатывать крайние случаи, тогда как использование `gl.canvas.width` + и `gl.canvas.height` не будет. [Что касается почему, см. здесь](#drawingbuffer). + +2. Использование `canvas.width` и `canvas.height` для соотношения сторон + + Часто код использует `canvas.width` и `canvas.height` для соотношения сторон, как здесь: + + var aspect = canvas.width / canvas.height; + perspective(fieldOfView, aspect, zNear, zFar); + + **Почему это плохо:** + + Ширина и высота canvas не имеют ничего общего с размером, в котором canvas отображается. + CSS контролирует размер, в котором отображается canvas. + + **Что делать вместо этого:** + + Используйте `canvas.clientWidth` и `canvas.clientHeight`. Эти значения говорят вам, какого + размера ваш canvas фактически отображается на экране. Используя эти значения, + вы всегда получите правильное соотношение сторон независимо от настроек CSS. + + var aspect = canvas.clientWidth / canvas.clientHeight; + perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); + + Вот примеры canvas, чьи drawingbuffer'ы имеют одинаковый размер (`width="400" height="300"`), + но используя CSS мы сказали браузеру отображать canvas другого размера. + Обратите внимание, что образцы оба отображают 'F' в правильном соотношении сторон. + + {{{diagram url="../webgl-canvas-clientwidth-clientheight.html" width="150" height="200" }}} +

+ {{{diagram url="../webgl-canvas-clientwidth-clientheight.html" width="400" height="150" }}} + + Если бы мы использовали `canvas.width` и `canvas.height`, это было бы не так. + + {{{diagram url="../webgl-canvas-width-height.html" width="150" height="200" }}} +

+ {{{diagram url="../webgl-canvas-width-height.html" width="400" height="150" }}} + +3. Использование `window.innerWidth` и `window.innerHeight` для вычисления чего-либо + + Многие WebGL программы используют `window.innerWidth` и `window.innerHeight` во многих местах. + Например: + + canvas.width = window.innerWidth; // ПЛОХО!! + canvas.height = window.innerHeight; // ПЛОХО!! + + **Почему это плохо:** + + Это не переносимо. Да, это может работать для WebGL страниц, где вы хотите сделать canvas + заполняющим экран. Проблема возникает, когда вы этого не делаете. Может быть, вы решите сделать статью + как эти уроки, где ваш canvas - это просто небольшая диаграмма на большей странице. + Или, может быть, вам нужен редактор свойств сбоку или счет для игры. Конечно, вы можете исправить свой код + для обработки этих случаев, но почему бы просто не написать его так, чтобы он работал в этих случаях с самого начала? + Тогда вам не придется изменять какой-либо код, когда вы копируете его в новый проект или используете старый + проект по-новому. + + **Что делать вместо этого:** + + Вместо того чтобы бороться с веб-платформой, используйте веб-платформу так, как она была предназначена для использования. + Используйте CSS и `clientWidth` и `clientHeight`. + + var width = gl.canvas.clientWidth; + var height = gl.canvas.clientHeight; + + gl.canvas.width = width; + gl.canvas.height = height; + + Вот 9 случаев. Все они используют точно такой же код. Обратите внимание, что ни один из них + не ссылается на `window.innerWidth` или `window.innerHeight`. + + Страница только с canvas, использующая CSS для полноэкранного режима + + Страница с canvas, установленным на использование 70% ширины, чтобы было место для элементов управления редактора + + Страница с canvas, встроенным в абзац + + Страница с canvas, встроенным в абзац, использующим box-sizing: border-box; + + box-sizing: border-box; заставляет границы и отступы занимать место от элемента, на котором они определены, а не снаружи него. Другими словами, в + обычном режиме box-sizing элемент 400x300 пикселей с 15-пиксельной границей имеет 400x300 пикселей пространства содержимого, окруженного 15-пиксельной границей, что делает его общий размер + 430x330 пикселей. В режиме box-sizing: border-box граница идет изнутри, так что тот же элемент останется 400x300 пикселей, содержимое окажется + 370x270. Это еще одна причина, почему использование `clientWidth` и `clientHeight` так важно. Если вы установите границу, скажем, `1em`, у вас не будет + способа узнать, какого размера окажется ваш canvas. Это было бы разным с разными шрифтами на разных машинах или разных браузерах. + + Страница только с контейнером, использующим CSS для полноэкранного режима, в который код вставит canvas + + Страница с контейнером, установленным на использование 70% ширины, чтобы было место для элементов управления редактора, в который код вставит canvas + + Страница с контейнером, встроенным в абзац, в который код вставит canvas + + Страница с контейнером, встроенным в абзац, использующим box-sizing: border-box;, в который код вставит canvas + + Страница без элементов с настройкой CSS для полноэкранного режима, в которую код вставит canvas + + Опять же, суть в том, что если вы принимаете веб и пишете свой код, используя методы выше, вам не придется изменять какой-либо код, когда вы столкнетесь с разными случаями использования. + +4. Использование события `'resize'` для изменения размера вашего canvas. + + Некоторые приложения проверяют событие `'resize'` окна, как здесь, для изменения размера их canvas. + + window.addEventListener('resize', resizeTheCanvas); + + или так: + + window.onresize = resizeTheCanvas; + + **Почему это плохо:** + + Это не плохо само по себе, скорее, для *большинства* WebGL программ это подходит для меньшего количества случаев использования. + Конкретно `'resize'` работает только когда окно изменяет размер. Это не работает, + если canvas изменяет размер по какой-то другой причине. Например, скажем, вы делаете + 3D редактор. У вас canvas слева и настройки справа. Вы сделали так, что есть перетаскиваемая полоса, + разделяющая 2 части, и вы можете перетащить эту полосу, чтобы сделать область настроек больше или меньше. + В этом случае вы не получите никаких событий `'resize'`. Аналогично, если у вас есть страница, где другое содержимое + добавляется или удаляется, и canvas изменяет размер, когда браузер перераспределяет страницу, вы не получите событие resize. + + **Что делать вместо этого:** + + Как и многие решения анти-паттернов выше, есть способ написать ваш код + так, чтобы он просто работал для большинства случаев. Для WebGL приложений, которые постоянно рисуют каждый кадр, + решение - проверять, нужно ли изменять размер каждый раз, когда вы рисуете, как здесь: + + function resizeCanvasToDisplaySize() { + var width = gl.canvas.clientWidth; + var height = gl.canvas.clientHeight; + if (gl.canvas.width != width || + gl.canvas.height != height) { + gl.canvas.width = width; + gl.canvas.height = height; + } + } + + function render() { + resizeCanvasToDisplaySize(); + drawStuff(); + requestAnimationFrame(render); + } + render(); + + Теперь в любом из этих случаев ваш canvas будет масштабироваться до правильного размера. Нет необходимости + изменять какой-либо код для разных случаев. Например, используя тот же код из #3 выше, + вот редактор с изменяемой областью редактирования. + + {{{example url="../webgl-same-code-resize.html" }}} + + Не было бы событий resize для этого случая, ни для любого другого, где canvas изменяет размер + на основе размера других динамических элементов на странице. + + Для WebGL приложений, которые не перерисовывают каждый кадр, код выше все еще правильный, вам просто нужно + запустить перерисовку в каждом случае, где canvas может потенциально изменить размер. Один простой способ - использовать `ResizeObserver` + +
+    const resizeObserver = new ResizeObserver(render);
+    resizeObserver.observe(gl.canvas, {box: 'content-box'});
+    
+ +5. Добавление свойств к `WebGLObject`'ам + + `WebGLObject`'ы - это различные типы ресурсов в WebGL, такие как `WebGLBuffer` + или `WebGLTexture`. Некоторые приложения добавляют свойства к этим объектам. Например, код вроде этого: + + var buffer = gl.createBuffer(); + buffer.itemSize = 3; // ПЛОХО!! + buffer.numComponents = 75; // ПЛОХО!! + + var program = gl.createProgram(); + ... + program.u_matrixLoc = gl.getUniformLocation(program, "u_matrix"); // ПЛОХО!! + + **Почему это плохо:** + + Причина, по которой это плохо, в том, что WebGL может "потерять контекст". Это может произойти по любой + причине, но самая распространенная причина - если браузер решит, что используется слишком много ресурсов GPU, + он может намеренно потерять контекст на некоторых `WebGLRenderingContext`'ах, чтобы освободить место. + WebGL программы, которые хотят всегда работать, должны обрабатывать это. Google Maps обрабатывает это, например. + + Проблема с кодом выше в том, что когда контекст потерян, WebGL функции создания, такие как + `gl.createBuffer()` выше, будут возвращать `null`. Это эффективно делает код таким: + + var buffer = null; + buffer.itemSize = 3; // ОШИБКА! + buffer.numComponents = 75; // ОШИБКА! + + Это, вероятно, убьет ваше приложение с ошибкой вроде: + + TypeError: Cannot set property 'itemSize' of null + + Хотя многим приложениям все равно, если они умрут, когда контекст потерян, кажется плохой идеей + писать код, который придется исправлять позже, если разработчики когда-либо решат обновить их + приложение для обработки событий потери контекста. + + **Что делать вместо этого:** + + Если вы хотите держать `WebGLObject`'ы и некоторую информацию о них вместе, один способ был бы + использовать JavaScript объекты. Например: + + var bufferInfo = { + id: gl.createBuffer(), + itemSize: 3, + numComponents: 75, + }; + + var programInfo = { + id: program, + u_matrixLoc: gl.getUniformLocation(program, "u_matrix"), + }; + + Лично я бы предложил [использовать несколько простых помощников, которые делают написание WebGL + намного проще](webgl-less-code-more-fun.html). + +Это несколько из того, что я считаю WebGL анти-паттернами в коде, который я видел в сети. +Надеюсь, я объяснил, почему их следует избегать, и дал решения, которые просты и полезны. + +

Что такое drawingBufferWidth и drawingBufferHeight?

+

+GPU имеют ограничение на то, насколько большим прямоугольником пикселей (текстура, renderbuffer) они могут поддерживать. Часто этот +размер - это следующая степень 2 больше, чем какое-либо распространенное разрешение монитора было во время создания GPU. +Например, если GPU был разработан для поддержки экранов 1280x1024, он может иметь ограничение размера 2048. +Если он был разработан для экранов 2560x1600, он может иметь ограничение 4096. +

+Это кажется разумным, но что происходит, если у вас несколько мониторов? Скажем, у меня есть GPU с ограничением +2048, но у меня два монитора 1920x1080. Пользователь открывает окно браузера с WebGL страницей, затем +растягивает это окно на оба монитора. Ваш код пытается установить canvas.width в +canvas.clientWidth, который в этом случае равен 3840. Что должно произойти? +

+

Сразу на ум приходят только 3 варианта

+
    +
  1. +

    Выбросить исключение.

    +

    Это кажется плохим. Большинство веб-приложений не будут проверять это, и приложение упадет. + Если у приложения были пользовательские данные, пользователь только что потерял свои данные

    +
  2. +
  3. +

    Ограничить размер canvas до лимита GPU

    +

    Проблема с этим решением в том, что это также + вероятно приведет к краху или, возможно, к испорченной веб-странице, потому что код ожидает, что canvas будет того размера, + который они запросили, и они ожидают, что другие части UI и элементы на странице будут в правильных местах.

    +
  4. +
  5. +

    Позволить canvas быть того размера, который запросил пользователь, но сделать его drawingbuffer лимитом

    +

    Это решение, которое использует WebGL. Если ваш код написан правильно, единственное, что пользователь может заметить, + это то, что изображение в canvas немного масштабируется. В противном случае это просто работает. В худшем случае большинство WebGL программ, которые + не делают правильную вещь, просто будут иметь немного неправильный дисплей, но если пользователь изменит размер окна обратно вниз, + вещи вернутся к нормальному состоянию.

    +
  6. +
+

У большинства людей нет нескольких мониторов, поэтому эта проблема редко возникает. Или, по крайней мере, раньше. +Chrome и Safari, по крайней мере, по состоянию на январь 2015 года, имели жестко закодированное ограничение на размер canvas в 4096. Apple's +5k iMac превышает этот лимит. Много WebGL приложений имели странные дисплеи из-за этого. +Аналогично многие люди начали использовать WebGL с несколькими мониторами для установочной работы и +сталкивались с этим лимитом.

+

+Итак, если вы хотите обрабатывать эти случаи, используйте gl.drawingBufferWidth и gl.drawingBufferHeight, как +показано в #1 выше. Для большинства приложений, если вы следуете лучшим практикам выше, вещи просто будут работать. Имейте в виду, +хотя, если вы делаете вычисления, которые должны знать фактический размер drawingbuffer, вам нужно +учитывать это. Примеры сразу на ум, [picking](webgl-picking.html), другими словами преобразование из +координат мыши в координаты пикселей canvas. Другим был бы любой вид пост-обработки +эффектов, которые хотят знать фактический размер drawingbuffer. +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-attributes.md b/webgl/lessons/ru/webgl-attributes.md new file mode 100644 index 000000000..e363c5eda --- /dev/null +++ b/webgl/lessons/ru/webgl-attributes.md @@ -0,0 +1,282 @@ +Title: WebGL2 Атрибуты +Description: Что такое атрибуты в WebGL? +TOC: Атрибуты + + +Эта статья предназначена для того, чтобы дать вам мысленное представление +о том, как настраивается состояние атрибутов в WebGL. Есть [похожая статья о единицах текстур](webgl-texture-units.html) и о [framebuffer'ах](webgl-framebuffers.html). + +Как предварительное условие вам, вероятно, стоит прочитать [Как работает WebGL](webgl-how-it-works.html) +и [WebGL Шейдеры и GLSL](https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html). + +## Атрибуты + +В WebGL атрибуты - это входы для vertex шейдера, которые получают свои данные из буферов. +WebGL будет выполнять пользовательский vertex шейдер N раз, когда вызывается либо `gl.drawArrays`, либо `gl.drawElements`. +Для каждой итерации атрибуты определяют, как извлекать данные из буферов, привязанных к ним, +и поставлять их к атрибутам внутри vertex шейдера. + +Если бы они были реализованы в JavaScript, они выглядели бы примерно так: + +```js +// псевдокод +const gl = { + arrayBuffer: null, + vertexArray: { + attributes: [ + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + ], + elementArrayBuffer: null, + }, +} +``` + +Как вы можете видеть выше, есть 16 атрибутов. + +Когда вы вызываете `gl.enableVertexAttribArray(location)` или `gl.disableVertexAttribArray`, вы можете думать об этом так: + +```js +// псевдокод +gl.enableVertexAttribArray = function(location) { + const attrib = gl.vertexArray.attributes[location]; + attrib.enable = true; +}; + +gl.disableVertexAttribArray = function(location) { + const attrib = gl.vertexArray.attributes[location]; + attrib.enable = false; +}; +``` + +Другими словами, location напрямую ссылается на индекс атрибута. + +Аналогично `gl.vertexAttribPointer` используется для установки почти всех остальных +настроек атрибута. Это было бы реализовано примерно так: + +```js +// псевдокод +gl.vertexAttribPointer = function(location, size, type, normalize, stride, offset) { + const attrib = gl.vertexArray.attributes[location]; + attrib.size = size; + attrib.type = type; + attrib.normalize = normalize; + attrib.stride = stride ? stride : sizeof(type) * size; + attrib.offset = offset; + attrib.buffer = gl.arrayBuffer; // !!!! <----- +}; +``` + +Обратите внимание, что когда мы вызываем `gl.vertexAttribPointer`, `attrib.buffer` +устанавливается в то, что в данный момент установлено в `gl.arrayBuffer`. +`gl.arrayBuffer` в псевдокоде выше был бы установлен вызовом +`gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer)`. + +```js +// псевдокод +gl.bindBuffer = function(target, buffer) { + switch (target) { + case ARRAY_BUFFER: + gl.arrayBuffer = buffer; + break; + case ELEMENT_ARRAY_BUFFER; + gl.vertexArray.elementArrayBuffer = buffer; + break; + ... +}; +``` + +Итак, дальше у нас есть vertex шейдеры. В vertex шейдере вы объявляете атрибуты. Пример: + +```glsl +#version 300 es +in vec4 position; +in vec2 texcoord; +in vec3 normal; + +... + +void main() { + ... +} +``` + +Когда вы связываете vertex шейдер с fragment шейдером, вызывая +`gl.linkProgram(someProgram)`, WebGL (драйвер/GPU/браузер) решают сами, +какой индекс/location использовать для каждого атрибута. Если вы не назначите +locations вручную (см. ниже), вы не знаете, какие они выберут. Это зависит от +браузера/драйвера/GPU. Итак, вам нужно спросить его, какой атрибут он использовал +для position, texcoord и normal? Вы делаете это, вызывая +`gl.getAttribLocation` + +```js +const positionLoc = gl.getAttribLocation(program, 'position'); +const texcoordLoc = gl.getAttribLocation(program, 'texcoord'); +const normalLoc = gl.getAttribLocation(program, 'normal'); +``` + +Допустим, `positionLoc` = `5`. Это означает, что когда vertex шейдер выполняется (когда +вы вызываете `gl.drawArrays` или `gl.drawElements`), vertex шейдер ожидает, что вы +настроили атрибут 5 с правильным типом, размером, смещением, шагом, буфером и т.д. + +Обратите внимание, что ДО связывания программы вы можете выбрать locations, вызывая +`gl.bindAttribLocation(program, location, nameOfAttribute)`. Пример: + +```js +// Скажите `gl.linkProgram` назначить `position` для использования атрибута #7 +gl.bindAttribLocation(program, 7, 'position'); +``` + +Вы также можете указать, какой location использовать в вашем шейдере напрямую, если вы +используете GLSL ES 3.00 шейдеры с: + +```glsl +layout(location = 0) in vec4 position; +layout(location = 1) in vec2 texcoord; +layout(location = 2) in vec3 normal; + +... +``` + +Кажется, что использование `bindAttribLocation` намного более [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), +но используйте что вам нравится. + +## Полное состояние атрибута + +Отсутствует в описании выше то, что каждый атрибут также имеет значение по умолчанию. +Это опущено выше, потому что это необычно использовать. + +```js +attributeValues: [ + [0, 0, 0, 1], + [0, 0, 0, 1], + ... +], +vertexArray: { + attributes: [ + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, +  divisor: 0, }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, +  divisor: 0, }, + ... +``` +Вы можете установить значение каждого атрибута с помощью различных функций `gl.vertexAttribXXX`. +Значение используется, когда `enable` равно false. Когда enable равно true, данные для +атрибута извлекаются из назначенного буфера. + + +## Vertex Array Objects (VAO) + +```js +const vao = gl.createVertexArray(); +``` + +создает объект, который вы видите прикрепленным к `gl.vertexArray` в *псевдокоде* +выше. Вызов `gl.bindVertexArray(vao)` назначает ваш созданный vertex array +объект как текущий vertex array. + +```js +// псевдокод +gl.bindVertexArray = function(vao) { + gl.vertexArray = vao ? vao : defaultVAO; +}; +``` + +Это позволяет вам установить все атрибуты и `ELEMENT_ARRAY_BUFFER` в +текущем VAO, так что когда вы хотите нарисовать определенную форму, это один вызов к +`gl.bindVertexArray` для эффективной настройки +всех атрибутов, тогда как в противном случае это было бы до одного вызова к обоим +`gl.bindBuffer`, `gl.vertexAttribPointer` (и возможно +`gl.enableVertexAttribArray`) **на атрибут**. + +Вы можете видеть, что это, возможно, хорошая вещь - использовать vertex array objects. +Чтобы использовать их, хотя часто требуется больше организации. Например, скажем, вы хотите +нарисовать куб с `gl.TRIANGLES` с одним шейдером, а затем снова с `gl.LINES` +с другим шейдером. Скажем, когда вы рисуете с треугольниками, вы используете +нормали для освещения, поэтому вы объявляете атрибуты в вашем шейдере так: + +```glsl +#version 300 es +// lighting-shader +// шейдер для куба, нарисованного с треугольниками + +in vec4 a_position; +in vec3 a_normal; +``` + +Затем вы используете эти позиции и нормали, как мы рассмотрели в +[первой статье об освещении](webgl-3d-lighting-directional.html) + +Для линий вы не хотите освещения, вы хотите сплошной цвет, поэтому вы +делаете что-то похожее на первые шейдеры на [первой странице](webgl-fundamentals.html) этих +уроков. Вы объявляете uniform для цвета. Это означает, что в вашем +vertex шейдере вам нужна только позиция + +```glsl +#version 300 es +// solid-shader +// шейдер для куба с линиями + +in vec4 a_position; +``` + +У нас нет представления о том, какие locations атрибутов будут решены для каждого шейдера. +Допустим, для lighting-shader выше locations: + +``` +a_position location = 1 +a_normal location = 0 +``` + +и для solid-shader, который имеет только один атрибут: + +``` +a_position location = 0 +``` + +Ясно, что при переключении шейдеров нам нужно будет настроить атрибуты по-разному. +Один шейдер ожидает, что данные `a_position` появятся на атрибуте 0. Другой шейдер +ожидает, что они появятся на атрибуте 1. + +Перенастройка атрибутов - это дополнительная работа. Хуже того, вся суть использования +vertex array object - это сэкономить нам от необходимости делать эту работу. Чтобы исправить эту проблему, +мы бы привязали locations до связывания программ шейдеров. + +Мы бы сказали WebGL: + +```js +gl.bindAttribLocation(solidProgram, 0, 'a_position'); +gl.bindAttribLocation(lightingProgram, 0, 'a_position'); +gl.bindAttribLocation(lightingProgram, 1, 'a_normal'); +``` + +**ДО вызова gl.linkProgram**. Это говорит WebGL, какие locations назначить при связывании шейдера. +Теперь мы можем использовать тот же VAO для обоих шейдеров. + +## Максимум атрибутов + +WebGL2 требует, чтобы поддерживалось как минимум 16 атрибутов, но конкретный +компьютер/браузер/реализация/драйвер может поддерживать больше. Вы можете узнать, +сколько поддерживается, вызвав: + +```js +const maxAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); +``` + +Если вы решите использовать больше 16, вам, вероятно, стоит проверить, сколько +фактически поддерживается, и сообщить пользователю, если их +машина не имеет достаточно, или иначе откатиться к более простым шейдерам. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-boilerplate.md b/webgl/lessons/ru/webgl-boilerplate.md new file mode 100644 index 000000000..2134dd45a --- /dev/null +++ b/webgl/lessons/ru/webgl-boilerplate.md @@ -0,0 +1,245 @@ +Title: WebGL2 Boilerplate +Description: Часть кода, которая нужна для всех WebGL программ +TOC: Boilerplate + + +Это продолжение статьи [WebGL Fundamentals](webgl-fundamentals.html). +WebGL иногда кажется сложным для изучения, потому что большинство уроков +охватывают все сразу. Я постараюсь избежать этого где возможно +и разбить на более мелкие части. + +Одна из вещей, которая делает WebGL сложным, это то, что у вас есть эти 2 +крошечные функции - вершинный шейдер и фрагментный шейдер. Эти две +функции выполняются на вашем GPU, откуда и берется вся скорость. +Поэтому они написаны на специальном языке, языке, который +соответствует тому, что может делать GPU. Эти 2 функции нужно скомпилировать и +связать. Этот процесс в 99% случаев одинаков во всех WebGL +программах. + +Вот boilerplate код для компиляции шейдера. + + /** + * Создает и компилирует шейдер. + * + * @param {!WebGLRenderingContext} gl WebGL контекст. + * @param {string} shaderSource GLSL исходный код для шейдера. + * @param {number} shaderType Тип шейдера, VERTEX_SHADER или + * FRAGMENT_SHADER. + * @return {!WebGLShader} Шейдер. + */ + function compileShader(gl, shaderSource, shaderType) { + // Создаем объект шейдера + var shader = gl.createShader(shaderType); + + // Устанавливаем исходный код шейдера. + gl.shaderSource(shader, shaderSource); + + // Компилируем шейдер + gl.compileShader(shader); + + // Проверяем, скомпилировался ли он + var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!success) { + // Что-то пошло не так во время компиляции; получаем ошибку + throw ("could not compile shader:" + gl.getShaderInfoLog(shader)); + } + + return shader; + } + +И boilerplate код для связывания 2 шейдеров в программу + + /** + * Создает программу из 2 шейдеров. + * + * @param {!WebGLRenderingContext) gl WebGL контекст. + * @param {!WebGLShader} vertexShader Вершинный шейдер. + * @param {!WebGLShader} fragmentShader Фрагментный шейдер. + * @return {!WebGLProgram} Программа. + */ + function createProgram(gl, vertexShader, fragmentShader) { + // создаем программу. + var program = gl.createProgram(); + + // прикрепляем шейдеры. + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + + // связываем программу. + gl.linkProgram(program); + + // Проверяем, связалась ли она. + var success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!success) { + // что-то пошло не так со связыванием; получаем ошибку + throw ("program failed to link:" + gl.getProgramInfoLog(program)); + } + + return program; + }; + +Конечно, то, как вы решите обрабатывать ошибки, может быть другим. Выбрасывание +исключений может быть не лучшим способом обработки. Тем не менее, эти несколько +строк кода практически одинаковы почти во всех WebGL программах. + +Теперь, когда многострочные шаблонные литералы поддерживаются во всех современных браузерах, +это мой предпочтительный способ хранения шейдеров. Я могу просто сделать что-то вроде + + var vertexShaderSource = `#version 300 es + + in vec4 a_position; + uniform mat4 u_matrix; + + void main() { + gl_Position = u_matrix * a_position; + } + `; + +И иметь легко редактируемый шейдер. Некоторые старые браузеры, как IE, не понравится +это, но во-первых, я использую WebGL, поэтому мне все равно на IE. Если бы я +заботился и имел fallback без WebGL, я бы использовал какой-то этап сборки с чем-то вроде +[Babel](https://babeljs.io/) для преобразования кода выше во что-то, что IE +понимает. + +В прошлом мне нравилось хранить мои шейдеры в не-javascript <script> тегах. +Это также делает их легкими для редактирования, поэтому я бы использовал код вроде этого. + + /** + * Создает шейдер из содержимого тега script. + * + * @param {!WebGLRenderingContext} gl WebGL контекст. + * @param {string} scriptId id тега script. + * @param {string} opt_shaderType. Тип шейдера для создания. + * Если не передан, будет использован атрибут type из + * тега script. + * @return {!WebGLShader} Шейдер. + */ + function createShaderFromScript(gl, scriptId, opt_shaderType) { + // ищем тег script по id. + var shaderScript = document.getElementById(scriptId); + if (!shaderScript) { + throw("*** Error: unknown script element" + scriptId); + } + + // извлекаем содержимое тега script. + var shaderSource = shaderScript.text; + + // Если мы не передали тип, используем 'type' из + // тега script. + if (!opt_shaderType) { + if (shaderScript.type == "x-shader/x-vertex") { + opt_shaderType = gl.VERTEX_SHADER; + } else if (shaderScript.type == "x-shader/x-fragment") { + opt_shaderType = gl.FRAGMENT_SHADER; + } else if (!opt_shaderType) { + throw("*** Error: shader type not set"); + } + } + + return compileShader(gl, shaderSource, opt_shaderType); + }; + +Теперь для компиляции шейдера я могу просто сделать + + var shader = compileShaderFromScript(gl, "someScriptTagId"); + +Я обычно иду на шаг дальше и делаю функцию для компиляции двух шейдеров +из тегов script, прикрепляю их к программе и связываю их. + + /** + * Создает программу из 2 тегов script. + * + * @param {!WebGLRenderingContext} gl WebGL контекст. + * @param {string} vertexShaderId id вершинного шейдера тега script. + * @param {string} fragmentShaderId id фрагментного шейдера тега script. + * @return {!WebGLProgram} Программа + */ + function createProgramFromScripts( + gl, vertexShaderId, fragmentShaderId) { + var vertexShader = createShaderFromScriptTag(gl, vertexShaderId, gl.VERTEX_SHADER); + var fragmentShader = createShaderFromScriptTag(gl, fragmentShaderId, gl.FRAGMENT_SHADER); + return createProgram(gl, vertexShader, fragmentShader); + } + +Другая часть кода, которую я использую почти в каждой WebGL программе - это что-то для +изменения размера canvas. Вы можете увидеть [как эта функция реализована здесь](webgl-resizing-the-canvas.html). + +В случае всех образцов эти 2 функции включены с помощью + + + +и используются так + + var program = webglUtils.createProgramFromScripts( + gl, [idOfVertexShaderScript, idOfFragmentShaderScript]); + + ... + + webglUtils.resizeCanvasToMatchDisplaySize(canvas); + +Кажется лучшим не засорять все образцы многими строками одного и того же кода, +так как они просто мешают тому, о чем этот конкретный пример. + +Фактический boilerplate API, используемый в большинстве этих образцов + + /** + * Создает программу из 2 источников. + * + * @param {WebGLRenderingContext} gl WebGLRenderingContext + * для использования. + * @param {string[]} shaderSources Массив источников для + * шейдеров. Первый предполагается вершинным шейдером, + * второй фрагментным шейдером. + * @param {string[]} [opt_attribs] Массив имен атрибутов. + * Локации будут назначены по индексу, если не переданы + * @param {number[]} [opt_locations] Локации для атрибутов. + * Параллельный массив к opt_attribs, позволяющий назначить локации. + * @param {module:webgl-utils.ErrorCallback} opt_errorCallback callback для ошибок. + * По умолчанию просто выводит ошибку в консоль + * при ошибке. Если вы хотите что-то другое, передайте callback. + * Ему передается сообщение об ошибке. + * @return {WebGLProgram} Созданная программа. + * @memberOf module:webgl-utils + */ + function createProgramFromSources(gl, + shaderSources, + opt_attribs, + opt_locations, + opt_errorCallback) + +где `shaderSources` - это массив строк, содержащий GLSL исходный код. +Первая строка в массиве - это исходный код вершинного шейдера. Вторая - это +исходный код фрагментного шейдера. + +Это большая часть моего минимального набора WebGL boilerplate кода. +[Вы можете найти код `webgl-utils.js` здесь](../resources/webgl-utils.js). +Если вы хотите что-то немного более организованное, проверьте [TWGL.js](https://twgljs.org). + +Остальное, что делает WebGL сложным - это настройка всех входов +для ваших шейдеров. Смотрите [как это работает](webgl-how-it-works.html). + +Я также предлагаю вам прочитать о [меньше кода больше веселья](webgl-less-code-more-fun.html) и проверить [TWGL](https://twgljs.org). + +Примечание, пока мы об этом, есть еще несколько скриптов по аналогичным причинам + +* [`webgl-lessons-ui.js`](../resources/webgl-lessons-ui.js) + + Это предоставляет код для настройки слайдеров, которые имеют видимое значение, которое обновляется при перетаскивании слайдера. + Снова я не хотел засорять все файлы этим кодом, поэтому он в одном месте. + +* [`lessons-helper.js`](../resources/lessons-helper.js) + + Этот скрипт не нужен, кроме как на webgl2fundamentals.org. Он помогает выводить сообщения об ошибках на + экран при использовании внутри live editor среди других вещей. + +* [`m3.js`](../resources/m3.js) + + Это куча 2d математических функций. Они создаются, начиная с первой статьи о + матричной математике, и когда они создаются, они встроены, но в конце концов их слишком много для засорения, + поэтому после нескольких примеров они используются путем включения этого скрипта. + +* [`m4.js`](../resources/m4.js) + + Это куча 3d математических функций. Они создаются, начиная с первой статьи о 3d + и когда они создаются, они встроены, но в конце концов их слишком много для засорения, поэтому после + второй статьи о 3d они используются путем включения этого скрипта. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-cors-permission.md b/webgl/lessons/ru/webgl-cors-permission.md new file mode 100644 index 000000000..cc28c6de1 --- /dev/null +++ b/webgl/lessons/ru/webgl-cors-permission.md @@ -0,0 +1,134 @@ +Title: WebGL2 - Cross Origin Images +Description: Использование изображений с разных доменов +TOC: Cross Origin Images + + +Эта статья является одной из серии статей о WebGL. Если вы не читали +их, я предлагаю [начать с более раннего урока](webgl-fundamentals.html). + +В WebGL часто нужно загружать изображения и затем загружать их в GPU для +использования в качестве текстур. Здесь было несколько образцов, которые делают это. Например, +статья о [обработке изображений](webgl-image-processing.html), статья о +[текстурах](webgl-3d-textures.html) и статья о +[реализации 2d drawImage](webgl-2d-drawimage.html). + +Обычно мы загружаем изображение примерно так + + // создает информацию о текстуре { width: w, height: h, texture: tex } + // Текстура начнется с 1x1 пикселей и будет обновлена + // когда изображение загрузится + function loadImageAndCreateTextureInfo(url) { + var tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + // Заполняем текстуру 1x1 синим пикселем. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255])); + + 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); + }); + img.src = url; + + return textureInfo; + } + +Проблема в том, что изображения могут содержать приватные данные (например, капчу, подпись, голое фото, ...). +Веб-страница часто имеет рекламу и другие вещи, не находящиеся под прямым контролем страницы, поэтому браузер должен предотвратить +этим вещам смотреть на содержимое этих приватных изображений. + +Простое использование `` не является проблемой, потому что хотя изображение будет отображаться браузером, +скрипт не может увидеть данные внутри изображения. [Canvas2D API](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) +имеет способ увидеть внутри изображения. Сначала вы рисуете изображение в canvas + + ctx.drawImage(someImg, 0, 0); + +Затем вы получаете данные + + var data = ctx.getImageData(0, 0, width, height); + +Но если изображение, которое вы нарисовали, пришло с другого домена, браузер пометит canvas как *загрязненный* и +вы получите ошибку безопасности при вызове `ctx.getImageData` + +WebGL должен сделать это еще на шаг дальше. В WebGL `gl.readPixels` - это эквивалентный вызов `ctx.getImageData`, +поэтому вы могли бы подумать, что может быть просто блокировка этого будет достаточной, но оказывается, что даже если вы не можете читать пиксели +напрямую, вы можете создавать шейдеры, которые работают дольше на основе цветов в изображении. Используя эту информацию +вы можете использовать тайминг для эффективного просмотра внутри изображения косвенно и выяснить его содержимое. + +Итак, WebGL просто запрещает все изображения, которые не с того же домена. Например, вот короткий образец, +который рисует вращающийся прямоугольник с текстурой с другого домена. +Обратите внимание, что текстура никогда не загружается, и мы получаем ошибку + +{{{example url="../webgl-cors-permission-bad.html" }}} + +Как мы обходим это? + +## Введите CORS + +CORS = Cross Origin Resource Sharing. Это способ для веб-страницы попросить сервер изображения разрешения +использовать изображение. + +Для этого мы устанавливаем атрибут `crossOrigin` в что-то, и затем когда браузер пытается получить +изображение с сервера, если это не тот же домен, браузер попросит разрешение CORS. + + + ... + + img.crossOrigin = ""; // просим разрешение CORS + img.src = url; + +Строка, которую вы устанавливаете в `crossOrigin`, отправляется на сервер. Сервер может посмотреть на эту строку и решить, +давать ли вам разрешение или нет. Большинство серверов, которые поддерживают CORS, не смотрят на строку, они просто +дают разрешение всем. Вот почему установка пустой строки работает. Все, что это означает в данном случае, +это "попросить разрешение" против, скажем, `img.crossOrigin = "bob"` означало бы "попросить разрешение для 'bob'". + +Почему мы не просто всегда запрашиваем это разрешение? Потому что запрос разрешения требует 2 HTTP запроса, поэтому это +медленнее, чем не запрашивать. Если мы знаем, что мы на том же домене, или мы знаем, что не будем использовать изображение ни для чего, +кроме тегов img и или canvas2d, то мы не хотим устанавливать `crossOrigin`, потому что это +сделает вещи медленнее. + +Мы можем сделать функцию, которая проверяет, является ли изображение, которое мы пытаемся загрузить, с того же источника, и если это не так, +устанавливает атрибут `crossOrigin`. + + function requestCORSIfNotSameOrigin(img, url) { + if ((new URL(url, window.location.href)).origin !== window.location.origin) { + img.crossOrigin = ""; + } + } + +И мы можем использовать это так + + ... + +requestCORSIfNotSameOrigin(img, url); + img.src = url; + + +{{{example url="../webgl-cors-permission-good.html" }}} + +Важно отметить, что запрос разрешения НЕ означает, что вам будет предоставлено разрешение. +Это зависит от сервера. Github pages дают разрешение, flickr.com дает разрешение, +imgur.com дает разрешение, но большинство веб-сайтов не дают. + +
+

Заставить Apache предоставить разрешение CORS

+

Если вы запускаете веб-сайт с apache и у вас установлен плагин mod_rewrite, +вы можете предоставить общее разрешение CORS, поместив

+
+    Header set Access-Control-Allow-Origin "*"
+
+

+В соответствующий файл .htaccess. +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-cross-platform-issues.md b/webgl/lessons/ru/webgl-cross-platform-issues.md new file mode 100644 index 000000000..e82f5d2d2 --- /dev/null +++ b/webgl/lessons/ru/webgl-cross-platform-issues.md @@ -0,0 +1,301 @@ +Title: WebGL2 Cross Platform Issues +Description: Вещи, о которых нужно знать при попытке сделать ваше WebGL приложение работающим везде. +TOC: Cross Platform Issues + +Вероятно, не будет шоком то, что не все WebGL программы работают на всех устройствах или +браузерах. + +Вот список большинства проблем, с которыми вы можете столкнуться, с верхушки моей головы + +## Производительность + +Топовый GPU, вероятно, работает в 100 раз быстрее, чем низкоуровневый GPU. Единственный способ обойти +это, который я знаю, это либо целиться низко, либо давать пользователю опции, как +делают большинство Desktop PC приложений, где они могут выбрать производительность или качество. + +## Память + +Аналогично топовый GPU может иметь от 12 до 24 гигабайт оперативной памяти, тогда как низкоуровневый GPU +вероятно имеет меньше 1 гигабайта. (Я старый, поэтому для меня удивительно, что низкоуровневый = 1 гигабайт, поскольку +я начал программировать на машинах с 16k до 64k памяти 😜) + +## Ограничения устройства + +WebGL имеет различные минимально поддерживаемые функции, но ваше локальное устройство может поддерживать +больше, чем этот минимум, что означает, что оно не сработает на других устройствах, которые поддерживают меньше. + +Примеры включают: + +* Максимальный размер текстуры + + 2048 или 4096 кажутся разумными ограничениями. По крайней мере, по состоянию на 2020 год выглядит, что + [99% устройств поддерживают 4096, но только 50% поддерживают > 4096](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE). + + Примечание: максимальный размер текстуры - это максимальное измерение, которое GPU может обработать. Это + не означает, что у GPU достаточно памяти для этого измерения в квадрате (для 2D + текстуры) или в кубе (для 3D текстуры). Например, некоторые GPU имеют максимальный размер + 16384. Но 3D текстура 16384 с каждой стороны потребовала бы 16 терабайт + памяти!!! + +* Максимальное количество вершинных атрибутов в одной программе + + В WebGL1 минимально поддерживается 8. В WebGL2 это 16. Если вы используете больше, чем это, + то ваш код не сработает на машине только с минимумом + +* Максимальное количество uniform векторов + + Они указаны отдельно для вершинных шейдеров и фрагментных шейдеров. + + В WebGL1 это 128 для вершинных шейдеров и 16 для фрагментных шейдеров + В WebGL2 это 256 для вершинных шейдеров и 224 для фрагментных шейдеров + + Обратите внимание, что uniforms могут быть "упакованы", поэтому число выше - это сколько `vec4` + можно использовать. Теоретически вы могли бы иметь в 4 раза больше `float` uniforms. + но есть алгоритм, который их помещает. Вы можете представить пространство как + массив с 4 столбцами, одна строка для каждого из максимальных uniform векторов выше. + + ``` + +-+-+-+-+ + | | | | | <- один vec4 + | | | | | | + | | | | | | + | | | | | V + | | | | | максимальные uniform векторы строк + | | | | | + | | | | | + | | | | | + ... + + ``` + + Сначала выделяются `vec4` с `mat4` как 4 `vec4`. Затем `vec3` помещаются + в оставшееся пространство. Затем `vec2` с последующими `float`. Так что представьте, что у нас было 1 + `mat4`, 2 `vec3`, 2 `vec2` и 3 `float` + + ``` + +-+-+-+-+ + |m|m|m|m| <- mat4 занимает 4 строки + |m|m|m|m| + |m|m|m|m| + |m|m|m|m| + |3|3|3| | <- 2 vec3 занимают 2 строки + |3|3|3| | + |2|2|2|2| <- 2 vec2 могут втиснуться в 1 строку + |f|f|f| | <- 3 float помещаются в одну строку + ... + + ``` + + Далее, массив uniforms всегда вертикальный, поэтому, например, если максимально + разрешенные uniform векторы - 16, то вы не можете иметь 17-элементный `float` массив + и на самом деле, если у вас был один `vec4`, это заняло бы целую строку, поэтому осталось + только 15 строк, что означает, что самый большой массив, который вы можете иметь, будет 15 + элементов. + + Мой совет, однако, не рассчитывайте на идеальную упаковку. Хотя спецификация говорит, что + алгоритм выше требуется для прохождения, слишком много комбинаций для тестирования, + что все драйверы проходят. Просто будьте в курсе, если вы приближаетесь к лимиту. + + примечание: varyings и attributes не могут быть упакованы. + +* Максимальные varying векторы. + + WebGL1 минимум 8. WebGL2 это 16. + + Если вы используете больше, ваш код не будет работать на машине только с минимумом. + +* Максимальные texture units + + Здесь 3 значения. + + 1. Сколько texture units существует + 2. Сколько texture units может ссылаться вершинный шейдер + 3. Сколько texture units может ссылаться фрагментный шейдер + + + + + + + + + + +
WebGL1WebGL2
мин texture units которые существуют832
мин texture units на которые может ссылаться вершинный шейдер0!16
мин texture units на которые может ссылаться фрагментный шейдер816
+ + Важно отметить **0** для вершинного шейдера в WebGL1. Обратите внимание, что это, вероятно, не конец света. + По-видимому, [~97% всех устройств поддерживают по крайней мере 4](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_TEXTURE_IMAGE_UNITS). + Тем не менее, вы можете захотеть проверить, чтобы вы могли либо сказать пользователю, что ваше приложение не будет работать для них, либо + вы можете откатиться к каким-то другим шейдерам. + +Есть и другие ограничения. Чтобы их посмотреть, вы вызываете `gl.getParameter` с +следующими значениями. + +
+ + + + + + + + + + + + + + +
MAX_TEXTURE_SIZE макс размер текстуры
MAX_VERTEX_ATTRIBS количество атрибутов которые вы можете иметь
MAX_VERTEX_UNIFORM_VECTORS количество vec4 uniforms которые может иметь вершинный шейдер
MAX_VARYING_VECTORS количество varyings которые у вас есть
MAX_COMBINED_TEXTURE_IMAGE_UNITSколичество texture units которые существуют
MAX_VERTEX_TEXTURE_IMAGE_UNITS количество texture units на которые может ссылаться вершинный шейдер
MAX_TEXTURE_IMAGE_UNITS количество texture units на которые может ссылаться фрагментный шейдер
MAX_FRAGMENT_UNIFORM_VECTORS количество vec4 uniforms которые может иметь фрагментный шейдер
MAX_CUBE_MAP_TEXTURE_SIZE макс размер cubemap
MAX_RENDERBUFFER_SIZE макс размер renderbuffer
MAX_VIEWPORT_DIMS макс размер viewport
+
+ +Это не весь список. Например, максимальный размер точки и максимальная толщина линии, +но вы должны в основном предполагать, что максимальная толщина линии 1.0 и что POINTS +полезны только для простых демо, где вам все равно на +[проблемы с обрезкой](#points-lines-viewport-scissor-behavior). + +WebGL2 добавляет еще несколько. Несколько общих: + +
+ + + + + + + + + + + +
MAX_3D_TEXTURE_SIZE макс размер 3D текстуры
MAX_DRAW_BUFFERS количество color attachments которые вы можете иметь
MAX_ARRAY_TEXTURE_LAYERS макс слоев в 2D texture array
MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS количество varyings которые вы можете выводить в отдельные буферы при использовании transform feedback
MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTSколичество varyings которые вы можете выводить при отправке их всех в один буфер
MAX_COMBINED_UNIFORM_BLOCKS количество uniform blocks которые вы можете использовать в целом
MAX_VERTEX_UNIFORM_BLOCKS количество uniform blocks которые может использовать вершинный шейдер
MAX_FRAGMENT_UNIFORM_BLOCKS количество uniform blocks которые может использовать фрагментный шейдер
+
+ +## Разрешение буфера глубины + +Несколько действительно старых мобильных устройств используют 16-битные буферы глубины. В противном случае, AFAICT 99% +устройств используют 24-битный буфер глубины, поэтому вам, вероятно, не нужно беспокоиться об +этом. + +## readPixels format/type комбинации + +Только определенные format/type комбинации гарантированно работают. Другие комбинации +опциональны. Это покрыто в [этой статье](webgl-readpixels.html). + +## framebuffer attachment комбинации + +Framebuffers могут иметь 1 или более attachments текстур и renderbuffers. + +В WebGL1 только 3 комбинации attachments гарантированно работают. + +1. один format = `RGBA`, type = `UNSIGNED_BYTE` texture как `COLOR_ATTACHMENT0` +2. format = `RGBA`, type = `UNSIGNED_BYTE` texture как `COLOR_ATTACHMENT0` и + format = `DEPTH_COMPONENT` renderbuffer прикрепленный как `DEPTH_ATTACHMENT` +3. format = `RGBA`, type = `UNSIGNED_BYTE` texture как `COLOR_ATTACHMENT0` и + format = `DEPTH_STENCIL` renderbuffer прикрепленный как `DEPTH_STENCIL_ATTACHMENT` + +Все другие комбинации зависят от реализации, которую вы проверяете, вызывая +`gl.checkFramebufferStatus` и видя, вернул ли он `FRAMEBUFFER_COMPLETE`. + +WebGL2 гарантирует возможность записи во многие другие форматы, но все еще имеет +ограничение в том, что **любая комбинация может потерпеть неудачу!** Ваша лучшая ставка может быть, если все +color attachments одного формата, если вы прикрепляете больше 1. + +## Расширения + +Многие функции WebGL1 и WebGL2 опциональны. Вся суть наличия +API под названием `getExtension` в том, что он может потерпеть неудачу, если расширение не существует, +и поэтому вы должны проверять эту неудачу и не слепо предполагать, что это +сработает. + +Вероятно, наиболее часто отсутствующее расширение в WebGL1 и WebGL2 - это +`OES_texture_float_linear`, что является способностью фильтровать floating point +текстуру, что означает способность поддерживать установку `TEXTURE_MIN_FILTER` и +`TEXTURE_MAX_FILTER` на что-либо, кроме `NEAREST`. Многие мобильные устройства не +поддерживают это. + +В WebGL1 другое часто отсутствующее расширение - это `WEBGL_draw_buffers`, что является +способностью прикреплять больше 1 color attachment к framebuffer, все еще около +70% для десктопа и почти ни одного для смартфонов (это кажется неправильным). +В основном любое устройство, которое может запускать WebGL2, должно также поддерживать +`WEBGL_draw_buffers` в WebGL1, но все же, это, по-видимому, все еще проблема. Если вы +нуждаетесь рендерить в несколько текстур одновременно, вероятно, вашей странице нужен +высокоуровневый GPU в целом. Тем не менее, вы должны проверить, поддерживает ли устройство пользователя это, и +если нет, предоставить дружелюбное объяснение. + +Для WebGL1 следующие 3 расширения кажутся почти универсально поддерживаемыми, поэтому, хотя +вы можете захотеть предупредить пользователя, что ваша страница не будет работать, если они +отсутствуют, вероятно, что у этого пользователя крайне старое устройство, которое все равно не собиралось +хорошо запускать вашу страницу. + +Это `ANGLE_instance_arrays` (способность использовать [instanced drawing](webgl-instanced-drawing.html)), +`OES_vertex_array_object` (способность хранить все состояние атрибута в объекте, чтобы вы могли поменять все +это состояние одним вызовом функции. Смотрите [это](webgl-attributes.html)), и `OES_element_index_uint` +(способность использовать `UNSIGNED_INT` 32 битные индексы с [`drawElements`](webgl-indexed-vertices.html)). + +## локации атрибутов + +Полу-общая ошибка - не искать локации атрибутов. Например, у вас есть вершинный шейдер вроде + +```glsl +attribute vec4 position; +attribute vec2 texcoord; + +uniform mat4 matrix; + +varying vec2 v_texcoord; + +void main() { + gl_Position = matrix * position; + v_texcoord = texcoord; +} +``` + +Ваш код предполагает, что `position` будет атрибутом 0 и `texcoord` будет +атрибутом 1, но это не гарантировано. Поэтому он работает для вас, но не срабатывает для кого-то +другого. Часто это может быть ошибкой в том, что вы не делали это намеренно, но +через ошибку в коде вещи работают, когда локации одним способом, но не +другим. + +Есть 3 решения. + +1. Всегда искать локации. +2. Назначать локации, вызывая `gl.bindAttribLocation` перед вызовом `gl.linkProgram` +3. Только WebGL2, устанавливать локации в шейдере как в + + ```glsl + #version 300 es + layout(location = 0) vec4 position; + latout(location = 1) vec2 texcoord; + ... + ``` + + Решение 2 кажется наиболее [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), тогда как решение 3 + кажется наиболее [W.E.T.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions), если только + вы не генерируете ваши текстуры во время выполнения. + +## GLSL неопределенное поведение + +Несколько GLSL функций имеют неопределенное поведение. Например, `pow(x, y)` является +неопределенным, если `x < 0`. Есть более длинный список в [низу статьи о +spot lighting](webgl-3d-lighting-spot.html). + +## Проблемы точности шейдеров + +В 2020 году самая большая проблема здесь в том, что если вы используете `mediump` или `lowp` в ваших шейдерах, +то на десктопе GPU действительно будет использовать `highp`, но на мобильных они действительно будут +`mediump` и или `lowp`, и поэтому вы не заметите никаких проблем при разработке на десктопе. + +Смотрите [эту статью для более подробной информации](webgl-precision-issues.html). + +## Поведение Points, Lines, Viewport, Scissor + +`POINTS` и `LINES` в WebGL могут иметь максимальный размер 1, и на самом деле для `LINES` +это теперь наиболее общее ограничение. Далее, обрезаются ли точки, когда их +центр находится вне viewport, определяется реализацией. Смотрите низ +[этой статьи](webgl-drawing-without-data.html#pointissues). + +Аналогично, обрезает ли viewport только вершины или также пиксели, является +неопределенным. Scissor всегда обрезает пиксели, поэтому включайте scissor test и устанавливайте +размер scissor, если вы устанавливаете viewport меньше, чем то, что вы рисуете, +и вы рисуете LINES или POINTS. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-cube-maps.md b/webgl/lessons/ru/webgl-cube-maps.md new file mode 100644 index 000000000..936176bb6 --- /dev/null +++ b/webgl/lessons/ru/webgl-cube-maps.md @@ -0,0 +1,219 @@ +Title: WebGL2 Кубические карты +Description: Как использовать кубические карты в WebGL +TOC: Кубические карты + + +Эта статья является частью серии статей о WebGL2. +[Первая статья начинается с основ](webgl-fundamentals.html). +Эта статья продолжается от [статьи о текстурах](webgl-3d-textures.html). +Эта статья также использует концепции, рассмотренные в [статье об освещении](webgl-3d-lighting-directional.html). +Если вы еще не читали эти статьи, возможно, вы захотите прочитать их сначала. + +В [предыдущей статье](webgl-3d-textures.html) мы рассмотрели, как использовать текстуры, +как они ссылаются координатами текстуры, которые идут от 0 до 1 поперек и вверх +текстуры, и как они фильтруются опционально с использованием мипмапов. + +Другой вид текстуры - это *кубическая карта*. Она состоит из 6 граней, представляющих +6 граней куба. Вместо традиционных координат текстуры, которые +имеют 2 измерения, кубическая карта использует нормаль, другими словами, 3D направление. +В зависимости от направления, в которое указывает нормаль, одна из 6 граней куба +выбирается, а затем в пределах этой грани пиксели сэмплируются для получения цвета. + +6 граней ссылаются по их направлению от центра куба. +Они + +```js +gl.TEXTURE_CUBE_MAP_POSITIVE_X +gl.TEXTURE_CUBE_MAP_NEGATIVE_X +gl.TEXTURE_CUBE_MAP_POSITIVE_Y +gl.TEXTURE_CUBE_MAP_NEGATIVE_Y +gl.TEXTURE_CUBE_MAP_POSITIVE_Z +gl.TEXTURE_CUBE_MAP_NEGATIVE_Z +``` + +Давайте сделаем простой пример, мы будем использовать 2D холст для создания изображений, используемых в +каждой из 6 граней. + +Вот некоторый код для заполнения холста цветом и центрированным сообщением + +```js +function generateFace(ctx, faceColor, textColor, text) { + const {width, height} = ctx.canvas; + ctx.fillStyle = faceColor; + ctx.fillRect(0, 0, width, height); + ctx.font = `${width * 0.7}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = textColor; + ctx.fillText(text, width / 2, height / 2); +} +``` + +И вот некоторый код для вызова его для генерации 6 изображений + +```js +// Получаем 2D контекст +/** @type {Canvas2DRenderingContext} */ +const ctx = document.createElement("canvas").getContext("2d"); + +ctx.canvas.width = 128; +ctx.canvas.height = 128; + +const faceInfos = [ + { faceColor: '#F00', textColor: '#0FF', text: '+X' }, + { faceColor: '#FF0', textColor: '#00F', text: '-X' }, + { faceColor: '#0F0', textColor: '#F0F', text: '+Y' }, + { faceColor: '#0FF', textColor: '#F00', text: '-Y' }, + { faceColor: '#00F', textColor: '#FF0', text: '+Z' }, + { faceColor: '#F0F', textColor: '#0F0', text: '-Z' }, +]; +faceInfos.forEach((faceInfo) => { + const {faceColor, textColor, text} = faceInfo; + generateFace(ctx, faceColor, textColor, text); + + // показываем результат + ctx.canvas.toBlob((blob) => { + const img = new Image(); + img.src = URL.createObjectURL(blob); + document.body.appendChild(img); + }); +}); +``` + +{{{example url="../webgl-cubemap-faces.html" }}} + +Теперь давайте применим это к кубу. Мы начнем с кода +из примера атласа текстур [в предыдущей статье](webgl-3d-textures.html). + +Сначала давайте изменим шейдеры, чтобы использовать кубическую карту + +```glsl +#version 300 es + +in vec4 a_position; + +uniform mat4 u_matrix; + +out vec3 v_normal; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_matrix * a_position; + + // Передаем нормаль. Поскольку позиции + // центрированы вокруг начала координат, мы можем просто + // передать позицию + v_normal = normalize(a_position.xyz); +} +``` + +Мы убрали координаты текстуры из шейдера и +добавили varying для передачи нормали в фрагментный шейдер. +Поскольку позиции нашего куба идеально центрированы вокруг начала координат, +мы можем просто использовать их как наши нормали. + +Напомним из [статьи об освещении](webgl-3d-lighting-directional.html), что +нормали - это направление и обычно используются для указания направления +поверхности некоторой вершины. Поскольку мы используем нормализованные позиции +для наших нормалей, если бы мы освещали это, мы получили бы плавное освещение по +кубу. Для нормального куба нам пришлось бы иметь разные нормали для каждой +вершины для каждой грани. + +{{{diagram url="resources/cube-normals.html" caption="стандартная нормаль куба vs нормали этого куба" }}} + +Поскольку мы не используем координаты текстуры, мы можем убрать весь код, связанный с +настройкой координат текстуры. + +В фрагментном шейдере нам нужно использовать `samplerCube` вместо `sampler2D`, +и `texture`, когда используется с `samplerCube`, принимает vec3 направление, +поэтому мы передаем нормализованную нормаль. Поскольку нормаль - это varying и будет интерполирована, +нам нужно нормализовать ее. + +``` +#version 300 es + +precision highp float; + +// Переданный из вершинного шейдера. +in vec3 v_normal; + +// Текстура. +uniform samplerCube u_texture; + +// нам нужно объявить выход для фрагментного шейдера +out vec4 outColor; + +void main() { + outColor = texture(u_texture, normalize(v_normal)); +} +``` + +Затем в JavaScript нам нужно настроить текстуру + +```js +// Создаем текстуру. +var texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); + +// Получаем 2D контекст +/** @type {Canvas2DRenderingContext} */ +const ctx = document.createElement("canvas").getContext("2d"); + +ctx.canvas.width = 128; +ctx.canvas.height = 128; + +const faceInfos = [ + { target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, faceColor: '#F00', textColor: '#0FF', text: '+X' }, + { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, faceColor: '#FF0', textColor: '#00F', text: '-X' }, + { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, faceColor: '#0F0', textColor: '#F0F', text: '+Y' }, + { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, faceColor: '#0FF', textColor: '#F00', text: '-Y' }, + { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, faceColor: '#00F', textColor: '#FF0', text: '+Z' }, + { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, faceColor: '#F0F', textColor: '#0F0', text: '-Z' }, +]; +faceInfos.forEach((faceInfo) => { + const {target, faceColor, textColor, text} = faceInfo; + generateFace(ctx, faceColor, textColor, text); + + // Загружаем холст в грань кубической карты. + const level = 0; + const internalFormat = gl.RGBA; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; + gl.texImage2D(target, level, internalFormat, format, type, ctx.canvas); +}); +gl.generateMipmap(gl.TEXTURE_CUBE_MAP); +gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); +``` + +Вещи для заметки выше: + +* Мы используем `gl.TEXTURE_CUBE_MAP` вместо `gl.TEXTURE_2D`. + + Это говорит WebGL сделать кубическую карту вместо 2D текстуры. + +* Для загрузки каждой грани текстуры мы используем специальные цели. + + `gl.TEXTURE_CUBE_MAP_POSITIVE_X`, + `gl.TEXTURE_CUBE_MAP_NEGATIVE_X`, + `gl.TEXTURE_CUBE_MAP_POSITIVE_Y`, + `gl.TEXTURE_CUBE_MAP_NEGATIVE_Y`, + `gl.TEXTURE_CUBE_MAP_POSITIVE_Z`, и + `gl.TEXTURE_CUBE_MAP_NEGATIVE_Z`. + +* Каждая грань - это квадрат. Выше они 128x128. + + Кубические карты должны иметь квадратные текстуры. + Мы также + генерируем мипмапы и включаем фильтрацию для использования мипмапов. + +И вуаля + +{{{example url="../webgl-cubemap.html" }}} + +Использование кубической карты для текстурирования куба - это **не** то, для чего кубические карты обычно +используются. *Правильный* или скорее стандартный способ текстурирования куба - это +использовать атлас текстур, как мы [упоминали раньше](webgl-3d-textures.html). + +Теперь, когда мы изучили, что такое кубическая карта и как ее настроить, для чего используется кубическая карта? +Вероятно, самая распространенная вещь, для которой используется кубическая карта, это как +[*карта окружения*](webgl-environment-maps.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-data-textures.md b/webgl/lessons/ru/webgl-data-textures.md new file mode 100644 index 000000000..c4b07d462 --- /dev/null +++ b/webgl/lessons/ru/webgl-data-textures.md @@ -0,0 +1,340 @@ +Title: WebGL2 3D - Data Textures +Description: Предоставление данных для текстуры. +TOC: Data Textures + + +Этот пост является продолжением серии постов о WebGL2. +Первый [начался с основ](webgl-fundamentals.html) +и предыдущий был о [текстурах](webgl-3d-textures.html). + +В последнем посте мы рассмотрели, как работают текстуры и как их применять. +Мы создавали их из изображений, которые загружали. В этой статье вместо +использования изображения мы создадим данные в JavaScript напрямую. + +Создание данных для текстуры в JavaScript в основном простое в зависимости +от формата текстуры. WebGL2 поддерживает тонну форматов текстур. +WebGL2 поддерживает все *неразмерные* форматы из WebGL1 + +
+ + + + + + + + + + + + + + +
FormatTypeChannelsBytes per pixel
RGBAUNSIGNED_BYTE44
RGBUNSIGNED_BYTE33
RGBAUNSIGNED_SHORT_4_4_4_442
RGBAUNSIGNED_SHORT_5_5_5_142
RGBUNSIGNED_SHORT_5_6_532
LUMINANCE_ALPHAUNSIGNED_BYTE22
LUMINANCEUNSIGNED_BYTE11
ALPHAUNSIGNED_BYTE11
+
+ +Они называются *неразмерными*, потому что то, как они фактически представлены внутренне, не определено в WebGL1. +Это определено в WebGL2. В дополнение к этим неразмерным форматам есть куча размерных форматов, включая + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sized
Format
Base
Format
R
bits
G
bits
B
bits
A
bits
Shared
bits
Color
renderable
Texture
filterable
R8 RED 8
R8_SNORM RED s8
RG8 RG 8 8
RG8_SNORM RG s8 s8
RGB8 RGB 8 8 8
RGB8_SNORM RGB s8 s8 s8
RGB565 RGB 5 6 5
RGBA4 RGBA 4 4 4 4
RGB5_A1 RGBA 5 5 5 1
RGBA8 RGBA 8 8 8 8
RGBA8_SNORM RGBA s8 s8 s8 s8
RGB10_A2 RGBA 10 10 10 2
RGB10_A2UI RGBA ui10 ui10 ui10ui2
SRGB8 RGB 8 8 8
SRGB8_ALPHA8 RGBA 8 8 8 8
R16F RED f16
RG16F RG f16 f16
RGB16F RGB f16 f16 f16
RGBA16F RGBA f16 f16 f16 f16
R32F RED f32
RG32F RG f32 f32
RGB32F RGB f32 f32 f32
RGBA32F RGBA f32 f32 f32 f32
R11F_G11F_B10F RGB f11 f11 f10
RGB9_E5 RGB 9 9 9 5
R8I RED i8
R8UI RED ui8
R16I RED i16
R16UI RED ui16
R32I RED i32
R32UI RED ui32
RG8I RG i8 i8
RG8UI RG ui8 ui8
RG16I RG i16 i16
RG16UI RG ui16 ui16
RG32I RG i32 i32
RG32UI RG ui32 ui32
RGB8I RGB i8 i8 i8
RGB8UI RGB ui8 ui8 ui8
RGB16I RGB i16 i16 i16
RGB16UI RGB ui16 ui16 ui16
RGB32I RGB i32 i32 i32
RGB32UI RGB ui32 ui32 ui32
RGBA8I RGBA i8 i8 i8 i8
RGBA8UI RGBA ui8 ui8 ui8 ui8
RGBA16I RGBA i16 i16 i16 i16
RGBA16UI RGBA ui16 ui16 ui16ui16
RGBA32I RGBA i32 i32 i32 i32
RGBA32UI RGBA ui32 ui32 ui32ui32
+
+ +И эти форматы глубины и трафарета также + +
+ + + + + + + + + + + + + + + + + +
Sized
Format
Base
Format
Depth
bits
Stencil
bits
DEPTH_COMPONENT16 DEPTH_COMPONENT 16
DEPTH_COMPONENT24 DEPTH_COMPONENT 24
DEPTH_COMPONENT32F DEPTH_COMPONENT f32
DEPTH24_STENCIL8 DEPTH_STENCIL 24 ui8
DEPTH32F_STENCIL8 DEPTH_STENCIL f32 ui8
+
+ +Легенда: + +* одно число как `8` означает 8 бит, которые будут нормализованы от 0 до 1 +* число, предшествующее `s` как `s8` означает знаковое 8-битное число, которое будет нормализовано от -1 до 1 +* число, предшествующее `f` как `f16` означает число с плавающей точкой. +* число, предшествующее `i` как `i8` означает целое число. +* число, предшествующее `ui` как `ui8` означает беззнаковое целое число. + +Мы не будем использовать эту информацию здесь, но я выделил +половинные и float форматы текстур, чтобы показать, в отличие от WebGL1, они всегда доступны в WebGL2, +но они не отмечены как color renderable и/или texture filterable по умолчанию. +Не быть color renderable означает, что они не могут быть отрендерены. [Рендеринг в текстуру покрыт +в другом уроке](webgl-render-to-texture.html). Не texture filterable означает, что они +должны использоваться только с `gl.NEAREST`. Обе эти функции доступны как опциональные +расширения в WebGL2. + +Для каждого из форматов вы указываете как *internal format* (формат, который GPU будет использовать внутренне), +так и *format* и *type* данных, которые вы предоставляете WebGL. Вот таблица, показывающая, какой format +и type вы должны предоставить данные для данного internal format + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Internal
Format
FormatTypeSource
Bytes
Per Pixel
RGBA8
RGB5_A1
RGBA4
SRGB8_ALPHA8
RGBA UNSIGNED_BYTE 4
RGBA8_SNORM RGBA BYTE 4
RGBA4 RGBA UNSIGNED_SHORT_4_4_4_4 2
RGB5_A1 RGBA UNSIGNED_SHORT_5_5_5_1 2
RGB10_A2
RGB5_A1
RGBA UNSIGNED_INT_2_10_10_10_REV 4
RGBA16F RGBA HALF_FLOAT 8
RGBA32F
RGBA16F
RGBA FLOAT 16
RGBA8UI RGBA_INTEGER UNSIGNED_BYTE 4
RGBA8I RGBA_INTEGER BYTE 4
RGBA16UI RGBA_INTEGER UNSIGNED_SHORT 8
RGBA16I RGBA_INTEGER SHORT 8
RGBA32UI RGBA_INTEGER UNSIGNED_INT 16
RGBA32I RGBA_INTEGER INT 16
RGB10_A2UI RGBA_INTEGER UNSIGNED_INT_2_10_10_10_REV 4
RGB8
RGB565
SRGB8
RGB UNSIGNED_BYTE 3
RGB8_SNORM RGB BYTE 3
RGB565 RGB UNSIGNED_SHORT_5_6_5 2
R11F_G11F_B10F RGB UNSIGNED_INT_10F_11F_11F_REV 4
RGB9_E5 RGB UNSIGNED_INT_5_9_9_9_REV 4
RGB16F
R11F_G11F_B10F
RGB9_E5
RGB HALF_FLOAT 6
RGB32F
RGB16F
R11F_G11F_B10F
RGB9_E5
RGB FLOAT 12
RGB8UI RGB_INTEGER UNSIGNED_BYTE 3
RGB8I RGB_INTEGER BYTE 3
RGB16UI RGB_INTEGER UNSIGNED_SHORT 6
RGB16I RGB_INTEGER SHORT 6
RGB32UI RGB_INTEGER UNSIGNED_INT 12
RGB32I RGB_INTEGER INT 12
RG8 RG UNSIGNED_BYTE 2
RG8_SNORM RG BYTE 2
RG16F RG HALF_FLOAT 4
RG32F
RG16F
RG FLOAT 8
RG8UI RG_INTEGER UNSIGNED_BYTE 2
RG8I RG_INTEGER BYTE 2
RG16UI RG_INTEGER UNSIGNED_SHORT 4
RG16I RG_INTEGER SHORT 4
RG32UI RG_INTEGER UNSIGNED_INT 8
RG32I RG_INTEGER INT 8
R8 RED UNSIGNED_BYTE 1
R8_SNORM RED BYTE 1
R16F RED HALF_FLOAT 2
R32F
R16F
RED FLOAT 4
R8UI RED_INTEGER UNSIGNED_BYTE 1
R8I RED_INTEGER BYTE 1
R16UI RED_INTEGER UNSIGNED_SHORT 2
R16I RED_INTEGER SHORT 2
R32UI RED_INTEGER UNSIGNED_INT 4
R32I RED_INTEGER INT 4
DEPTH_COMPONENT16 DEPTH_COMPONENT UNSIGNED_SHORT 2
DEPTH_COMPONENT24
DEPTH_COMPONENT16
DEPTH_COMPONENT UNSIGNED_INT 4
DEPTH_COMPONENT32F DEPTH_COMPONENT FLOAT 4
DEPTH24_STENCIL8 DEPTH_STENCIL UNSIGNED_INT_24_8 4
DEPTH32F_STENCIL8 DEPTH_STENCIL FLOAT_32_UNSIGNED_INT_24_8_REV 8
RGBA RGBA UNSIGNED_BYTE 4
RGBA RGBA UNSIGNED_SHORT_4_4_4_4 2
RGBA RGBA UNSIGNED_SHORT_5_5_5_1 2
RGB RGB UNSIGNED_BYTE 3
RGB RGB UNSIGNED_SHORT_5_6_5 2
LUMINANCE_ALPHA LUMINANCE_ALPHA UNSIGNED_BYTE 2
LUMINANCE LUMINANCE UNSIGNED_BYTE 1
ALPHA ALPHA UNSIGNED_BYTE 1
+
+ + +Давайте создадим 3x2 пиксельную `R8` текстуру. Поскольку это `R8` текстура, +есть только 1 значение на пиксель в красном канале. + +Мы возьмем образец из [последней статьи](webgl-3d-textures.html). Сначала мы изменим +координаты текстуры, чтобы использовать всю текстуру на каждой грани куба. + +``` +// Заполняем буфер координатами текстуры куба. +function setTexcoords(gl) { + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + // передняя грань + 0, 0, + 0, 1, + 1, 0, + 1, 0, + 0, 1, + 1, 1, + ... +``` + +Затем мы изменим код, который создает текстуру + +``` +// Создаем текстуру. +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])); + +// заполняем текстуру 3x2 пикселями +const level = 0; +const internalFormat = gl.R8; +const width = 3; +const height = 2; +const border = 0; +const format = gl.RED; +const type = gl.UNSIGNED_BYTE; +const data = new Uint8Array([ + 128, 64, 128, + 0, 192, 0, +]); +gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, + format, type, data); + +// устанавливаем фильтрацию, чтобы нам не нужны были мипы и она не фильтровалась +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + +-// Асинхронно загружаем изображение +-... +``` + +И вот это + +{{{example url="../webgl-data-texture-3x2-bad.html" }}} + +Упс! Почему это не работает?!?!? + +Проверяя JavaScript консоль, мы видим ошибку что-то вроде этого + +``` +WebGL: INVALID_OPERATION: texImage2D: ArrayBufferView not big enough for request +``` + +Оказывается, есть своего рода неясная настройка в WebGL, оставшаяся +с тех пор, когда OpenGL был впервые создан. Компьютеры иногда +работают быстрее, когда данные определенного размера. Например, может +быть быстрее копировать 2, 4 или 8 байт за раз вместо 1 за раз. +WebGL по умолчанию использует 4 байта за раз, поэтому он ожидает, что каждая +строка данных будет кратна 4 байтам (кроме последней строки). + +Наши данные выше только 3 байта на строку, 6 байт всего, но WebGL +собирается попытаться прочитать 4 байта для первой строки и 3 байта +для второй строки, всего 7 байт, поэтому он жалуется. + +Мы можем сказать WebGL обрабатывать 1 байт за раз так + + const alignment = 1; + gl.pixelStorei(gl.UNPACK_ALIGNMENT, alignment); + +Допустимые значения alignment: 1, 2, 4 и 8. + +Я подозреваю, что в WebGL вы не сможете измерить разницу +в скорости между выровненными данными и невыровненными данными. Я хотел бы, чтобы по умолчанию +было 1 вместо 4, чтобы эта проблема не кусала новых пользователей, но, чтобы +остаться совместимым с OpenGL, по умолчанию нужно было остаться тем же. +Таким образом, если портированное приложение предоставляет дополненные строки, оно будет работать без изменений. +В то же время, в новом приложении вы можете просто всегда устанавливать это в `1` и +затем покончить с этим. + +С этим установленным вещи должны работать + +{{{example url="../webgl-data-texture-3x2.html" }}} + +И с этим покрытым давайте перейдем к [рендерингу в текстуру](webgl-render-to-texture.html). + +
+

Пиксель vs Тексель

+

Иногда пиксели в текстуре называются текселями. Пиксель - это сокращение от Picture Element. +Тексель - это сокращение от Texture Element. +

+

Я уверен, что получу нагоняй от какого-нибудь гуру графики, но насколько я могу судить, "тексель" - это пример жаргона. +Лично я обычно использую "пиксель" при обращении к элементам текстуры, не думая об этом. 😇 +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-drawing-multiple-things.md b/webgl/lessons/ru/webgl-drawing-multiple-things.md new file mode 100644 index 000000000..f4b4cc920 --- /dev/null +++ b/webgl/lessons/ru/webgl-drawing-multiple-things.md @@ -0,0 +1,432 @@ +Title: WebGL2 - Рисование множественных объектов +Description: Как рисовать множество различных типов объектов в WebGL +TOC: Рисование множественных объектов + + +Эта статья является продолжением [предыдущих статей о WebGL](webgl-fundamentals.html). +Если вы их не читали, я предлагаю начать оттуда. + +Один из самых распространенных вопросов после того, как что-то впервые заработало в WebGL, это как +рисовать множество объектов. + +Первое, что нужно понять, это то, что с немногими исключениями, WebGL похож на функцию, +которую кто-то написал, где вместо передачи множества параметров в функцию у вас вместо этого +есть одна функция, которая рисует вещи, и 70+ функций, которые настраивают состояние для +этой одной функции. Так, например, представьте, что у вас есть функция, которая рисует круг. Вы +могли бы запрограммировать её так + + function drawCircle(centerX, centerY, radius, color) { ... } + +Или вы могли бы закодировать её так + + var centerX; + var centerY; + var radius; + var color; + + function setCenter(x, y) { + centerX = x; + centerY = y; + } + + function setRadius(r) { + radius = r; + } + + function setColor(c) { + color = c; + } + + function drawCircle() { + ... + } + +WebGL работает вторым способом. Функции типа `gl.createBuffer`, `gl.bufferData`, `gl.createTexture`, +и `gl.texImage2D` позволяют вам загружать данные в буферы (данные вершин) и данные в текстуры (цвет, и т.д.). +`gl.createProgram`, `gl.createShader`, `gl.compileShader`, и `gl.linkProgram` позволяют вам создавать +ваши GLSL шейдеры. Почти все остальные функции WebGL настраивают эти глобальные +переменные или *состояние*, которое используется когда `gl.drawArrays` или `gl.drawElements` наконец вызывается. + +Зная это, типичная программа WebGL в основном следует этой структуре + +Во время инициализации + +* создать все шейдеры и программы и найти местоположения +* создать буферы и загрузить данные вершин +* создать vertex array для каждой вещи, которую вы хотите нарисовать + * для каждого атрибута вызвать `gl.bindBuffer`, `gl.vertexAttribPointer`, `gl.enableVertexAttribArray` + * привязать любые индексы к `gl.ELEMENT_ARRAY_BUFFER` +* создать текстуры и загрузить данные текстур + +Во время рендеринга + +* очистить и установить viewport и другое глобальное состояние (включить depth testing, включить culling, и т.д.) +* Для каждой вещи, которую вы хотите нарисовать + * вызвать `gl.useProgram` для нужной программы для рисования. + * привязать vertex array для этой вещи. + * вызвать `gl.bindVertexArray` + * настроить uniforms для вещи, которую вы хотите нарисовать + * вызвать `gl.uniformXXX` для каждого uniform + * вызвать `gl.activeTexture` и `gl.bindTexture` чтобы назначить текстуры texture units. + * вызвать `gl.drawArrays` или `gl.drawElements` + +Вот и все. Вам решать, как организовать ваш код для выполнения этой задачи. + +Некоторые вещи, такие как загрузка данных текстур (и возможно даже данных вершин), могут происходить асинхронно, потому что +вам нужно ждать, пока они загрузятся по сети. + +Давайте сделаем простое приложение для рисования 3 вещей. Куб, сферу и конус. + +Я не буду вдаваться в детали того, как вычислять данные куба, сферы и конуса. Давайте просто +предположим, что у нас есть функции для их создания, и они возвращают [объекты bufferInfo, как описано в +предыдущей статье](webgl-less-code-more-fun.html). + +Итак, вот код. Наш шейдер тот же простой шейдер из нашего [примера с перспективой](webgl-3d-perspective.html), +кроме того, что мы добавили `u_colorMult` чтобы умножать цвета вершин. + + #version 300 es + precision highp float; + + // Передается из вершинного шейдера. + in vec4 v_color; + + uniform vec4 u_colorMult; + + out vec4 outColor; + + void main() { + * outColor = v_color * u_colorMult; + } + + +Во время инициализации + + // Наши uniforms для каждой вещи, которую мы хотим нарисовать + var sphereUniforms = { + u_colorMult: [0.5, 1, 0.5, 1], + u_matrix: m4.identity(), + }; + var cubeUniforms = { + u_colorMult: [1, 0.5, 0.5, 1], + u_matrix: m4.identity(), + }; + var coneUniforms = { + u_colorMult: [0.5, 0.5, 1, 1], + u_matrix: m4.identity(), + }; + + // Перевод для каждого объекта. + var sphereTranslation = [ 0, 0, 0]; + var cubeTranslation = [-40, 0, 0]; + var coneTranslation = [ 40, 0, 0]; + +Во время рисования + + var sphereXRotation = time; + var sphereYRotation = time; + var cubeXRotation = -time; + var cubeYRotation = time; + var coneXRotation = time; + var coneYRotation = -time; + + // ------ Рисуем сферу -------- + + gl.useProgram(programInfo.program); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(sphereVAO); + + sphereUniforms.u_matrix = computeMatrix( + viewProjectionMatrix, + sphereTranslation, + sphereXRotation, + sphereYRotation); + + // Устанавливаем uniforms, которые мы только что вычислили + twgl.setUniforms(programInfo, sphereUniforms); + + twgl.drawBufferInfo(gl, sphereBufferInfo); + + // ------ Рисуем куб -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(cubeVAO); + + cubeUniforms.u_matrix = computeMatrix( + viewProjectionMatrix, + cubeTranslation, + cubeXRotation, + cubeYRotation); + + // Устанавливаем uniforms, которые мы только что вычислили + twgl.setUniforms(programInfo, cubeUniforms); + + twgl.drawBufferInfo(gl, cubeBufferInfo); + + // ------ Рисуем конус -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(coneVAO); + + coneUniforms.u_matrix = computeMatrix( + viewProjectionMatrix, + coneTranslation, + coneXRotation, + coneYRotation); + + // Устанавливаем uniforms, которые мы только что вычислили + twgl.setUniforms(programInfo, coneUniforms); + + twgl.drawBufferInfo(gl, coneBufferInfo); + +И вот это + +{{{example url="../webgl-multiple-objects-manual.html" }}} + +Одна вещь, которую нужно заметить, это то, что поскольку у нас только одна программа шейдера, мы вызвали `gl.useProgram` +только один раз. Если бы у нас были разные программы шейдеров, вам нужно было бы вызвать `gl.useProgram` перед... эм... +использованием каждой программы. + +Это еще одно место, где хорошо упростить. Есть эффективно 4 основные вещи для комбинирования. + +1. Программа шейдера (и её информация о uniforms и атрибутах) +2. Vertex array (который содержит настройки атрибутов) +3. Uniforms, нужные для рисования этой вещи с данным шейдером. +4. Количество для передачи в gl.drawXXX и вызывать ли gl.drawArrays или gl.drawElements + +Итак, простое упрощение было бы сделать массив вещей для рисования и в этом массиве +поместить 4 вещи вместе + + var objectsToDraw = [ + { + programInfo: programInfo, + bufferInfo: sphereBufferInfo, + vertexArray: sphereVAO, + uniforms: sphereUniforms, + }, + { + programInfo: programInfo, + bufferInfo: cubeBufferInfo, + vertexArray: cubeVAO, + uniforms: cubeUniforms, + }, + { + programInfo: programInfo, + bufferInfo: coneBufferInfo, + vertexArray: coneVAO, + uniforms: coneUniforms, + }, + ]; + +Во время рисования нам все еще нужно обновлять матрицы + + var sphereXRotation = time; + var sphereYRotation = time; + var cubeXRotation = -time; + var cubeYRotation = time; + var coneXRotation = time; + var coneYRotation = -time; + + // Вычисляем матрицы для каждого объекта. + sphereUniforms.u_matrix = computeMatrix( + viewMatrix, + projectionMatrix, + sphereTranslation, + sphereXRotation, + sphereYRotation); + + cubeUniforms.u_matrix = computeMatrix( + viewMatrix, + projectionMatrix, + cubeTranslation, + cubeXRotation, + cubeYRotation); + + coneUniforms.u_matrix = computeMatrix( + viewMatrix, + projectionMatrix, + coneTranslation, + coneXRotation, + coneYRotation); + +Но код рисования теперь просто простой цикл + + // ------ Рисуем объекты -------- + + objectsToDraw.forEach(function(object) { + var programInfo = object.programInfo; + + gl.useProgram(programInfo.program); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(object.vertexArray); + + // Устанавливаем uniforms. + twgl.setUniforms(programInfo, object.uniforms); + + // Рисуем + twgl.drawBufferInfo(gl, bufferInfo); + }); + + +И это, возможно, основной цикл рендеринга большинства 3D движков в существовании. Где-то +какой-то код или коды решают, что попадает в список `objectsToDraw`, и количество +вариантов, которые им нужно, может быть больше, но большинство из них отделяют вычисление того, что +попадает в этот список, от фактического вызова функций `gl.draw___`. + +{{{example url="../webgl-multiple-objects-list.html" }}} + +В общем, считается *лучшей практикой* не вызывать WebGL избыточно. +Другими словами, если какое-то состояние WebGL уже установлено в то, что вам нужно, чтобы оно +было установлено, то не устанавливайте его снова. В этом духе мы могли бы проверить, если +программа шейдера, которая нам нужна для рисования текущего объекта, та же программа шейдера, +что и предыдущий объект, то нет необходимости вызывать `gl.useProgram`. Аналогично, +если мы рисуем той же формой/геометрией/вершинами, нет необходимости вызывать +`gl.bindVertexArray` + +Итак, очень простая оптимизация может выглядеть так + +```js +var lastUsedProgramInfo = null; +var lastUsedVertexArray = null; + +objectsToDraw.forEach(function(object) { + var programInfo = object.programInfo; + var vertexArray = object.vertexArray; + + if (programInfo !== lastUsedProgramInfo) { + lastUsedProgramInfo = programInfo; + gl.useProgram(programInfo.program); + } + + // Настраиваем все нужные атрибуты. + if (lastUsedVertexArray !== vertexArray) { + lastUsedVertexArray = vertexArray; + gl.bindVertexArray(vertexArray); + } + + // Устанавливаем uniforms. + twgl.setUniforms(programInfo, object.uniforms); + + // Рисуем + twgl.drawBufferInfo(gl, object.bufferInfo); +}); +``` + +На этот раз давайте нарисуем намного больше объектов. Вместо просто 3 как раньше давайте сделаем +список вещей для рисования больше + +```js +// помещаем формы в массив, чтобы легко выбирать их случайно +var shapes = [ + { bufferInfo: sphereBufferInfo, vertexArray: sphereVAO, }, + { bufferInfo: cubeBufferInfo, vertexArray: cubeVAO, }, + { bufferInfo: coneBufferInfo, vertexArray: coneVAO, }, +]; + +var objectsToDraw = []; +var objects = []; + +// Создаем информацию для каждого объекта для каждого объекта. +var baseHue = rand(360); +var numObjects = 200; +for (var ii = 0; ii < numObjects; ++ii) { + // выбираем форму + var shape = shapes[rand(shapes.length) | 0]; + + // создаем объект. + var object = { + uniforms: { + u_colorMult: chroma.hsv(emod(baseHue + rand(120), 360), rand(0.5, 1), rand(0.5, 1)).gl(), + u_matrix: m4.identity(), + }, + translation: [rand(-100, 100), rand(-100, 100), rand(-150, -50)], + xRotationSpeed: rand(0.8, 1.2), + yRotationSpeed: rand(0.8, 1.2), + }; + objects.push(object); + + // Добавляем его в список вещей для рисования. + objectsToDraw.push({ + programInfo: programInfo, + bufferInfo: shape.bufferInfo, + vertexArray: shape.vertexArray, + uniforms: object.uniforms, + }); +} +``` + +Во время рендеринга + +```js +// Вычисляем матрицы для каждого объекта. +objects.forEach(function(object) { + object.uniforms.u_matrix = computeMatrix( + viewProjectionMatrix, + object.translation, + object.xRotationSpeed * time, + object.yRotationSpeed * time); +}); +``` + +Затем рисуем объекты используя цикл выше. + +{{{example url="../webgl-multiple-objects-list-optimized.html" }}} + +> Примечание: Я изначально вырезал раздел выше из этой версии статьи WebGL2. +> [Оригинальная версия WebGL1 этой статьи](https://webglfundamentals.org/webgl/lessons/webgl-drawing-multiple-things.html) имела раздел об оптимизации. Причина, по которой я вырезал её, +> в том, что с vertex array objects я не так уверен, что оптимизации имеют большое значение. +> В WebGL1 без vertex arrays, рисование одного объекта часто требует +> 9 до 16 вызовов для настройки атрибутов для рисования объекта. В WebGL2 все это +> происходит во время инициализации путем настройки vertex array для каждого объекта, а затем во время рендеринга +> это один вызов `gl.bindVertexArray` для каждого объекта. +> +> Кроме того, в общем, большинство приложений WebGL не достигают предела рисования. Им +> нужно работать на множестве машин, от каких-то 8-летних низкоуровневых Intel +> интегрированных графических GPU до каких-то топовых машин. Оптимизации, упомянутые +> в разделе выше, вряд ли сделают разницу между производительными +> и не производительными. Скорее, для получения производительности требуется уменьшение количества +> вызовов рисования, например, используя [инстансинг](webgl-instanced-drawing.html) и +> другие подобные техники. +> +> Причина, по которой я добавил раздел обратно, в том, что было указано +> в отчете об ошибке, что последний пример, рисование 200 объектов, упоминается +> в [статье о пикинге](webgl-picking.html). 😅 + +## Рисование прозрачных вещей и множественных списков + +В примере выше есть только один список для рисования. Это работает, потому что все объекты +непрозрачные. Если мы хотим рисовать прозрачные объекты, они должны быть нарисованы сзади вперед +с самыми дальними объектами, нарисованными первыми. С другой стороны, для скорости, для непрозрачных +объектов мы хотим рисовать спереди назад, это потому что DEPTH_TEST означает, что GPU +не будет выполнять наш фрагментный шейдер для любых пикселей, которые были бы позади других вещей. +поэтому мы хотим нарисовать вещи спереди первыми. + +Большинство 3D движков обрабатывает это, имея 2 или более списков объектов для рисования. Один список для непрозрачных вещей. +Другой список для прозрачных вещей. Непрозрачный список сортируется спереди назад. +Прозрачный список сортируется сзади вперед. Также могут быть отдельные списки для других +вещей, таких как оверлеи или эффекты постобработки. + +## Рассмотрите использование библиотеки + +Важно заметить, что вы не можете рисовать любую геометрию любым шейдером. +Например, шейдер, который требует нормали, не будет работать с геометрией, у которой нет +нормалей. Аналогично, шейдер, который требует текстуры, не будет работать без текстур. + +Это одна из многих причин, почему здорово выбрать 3D библиотеку, такую как [Three.js](https://threejs.org), +потому что она обрабатывает все это за вас. Вы создаете некоторую геометрию, вы говорите three.js, как вы хотите её +рендерить, и она генерирует шейдеры во время выполнения для обработки вещей, которые вам нужны. Практически все 3D движки +делают это от Unity3D до Unreal до Source до Crytek. Некоторые генерируют их офлайн, но важная +вещь для понимания в том, что они *генерируют* шейдеры. + +Конечно, причина, по которой вы читаете эти статьи, в том, что вы хотите знать, что происходит глубоко внутри. +Это здорово и весело писать все самостоятельно. Просто важно осознавать, что +[WebGL супер низкоуровневый](webgl-2d-vs-3d-library.html), +поэтому есть тонна работы для вас, если вы хотите сделать это самостоятельно, и это часто включает +написание генератора шейдеров, поскольку разные функции часто требуют разных шейдеров. + +Вы заметите, что я не поместил `computeMatrix` внутрь цикла. Это потому что рендеринг должен +возможно быть отделен от вычисления матриц. Обычно вычисляют матрицы из +[scene graph, и мы рассмотрим это в другой статье](webgl-scene-graph.html). + +Теперь, когда у нас есть фреймворк для рисования множественных объектов, [давайте нарисуем немного текста](webgl-text-html.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-drawing-without-data.md b/webgl/lessons/ru/webgl-drawing-without-data.md new file mode 100644 index 000000000..cbd3e0ae8 --- /dev/null +++ b/webgl/lessons/ru/webgl-drawing-without-data.md @@ -0,0 +1,199 @@ +Title: WebGL2 Рисование без данных +Description: Креативное программирование - Рисование без данных +TOC: Рисование без данных + +Эта статья предполагает, что вы прочитали многие другие статьи, +начиная с [основ](webgl-fundamentals.html). +Если вы их не читали, пожалуйста, начните сначала с них. + +В [статье о самых маленьких WebGL программах](webgl-smallest-programs.html) +мы рассмотрели некоторые примеры рисования с очень небольшим количеством кода. +В этой статье мы рассмотрим рисование без данных. + +Традиционно WebGL приложения помещают геометрические данные в буферы. +Затем они используют атрибуты для извлечения данных вершин из этих буферов +в шейдеры и преобразования их в clip space. + +Слово **традиционно** важно. Это только **традиция** +делать это таким образом. Это никоим образом не требование. WebGL не +заботится о том, как мы это делаем, он заботится только о том, что наши вершинные шейдеры +присваивают координаты clip space `gl_Position`. + +В GLSL ES 3.0 есть специальная переменная `gl_VertexID`, +доступная в вершинных шейдерах. Эффективно она считает вершины. +Давайте используем её для рисования вычисления позиций вершин без данных. +Мы вычислим точки круга на основе этой переменной. + +```glsl +#version 300 es +uniform int numVerts; + +#define PI radians(180.0) + +void main() { + float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 + float angle = u * PI * 2.0; // идет от 0 до 2PI + float radius = 0.8; + + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + + gl_Position = vec4(pos, 0, 1); + gl_PointSize = 5.0; +} +``` + +Код выше должен быть довольно простым. +`gl_VertexID` будет считать от 0 до того количества +вершин, которое мы просим нарисовать. Мы передадим то же число +как `numVerts`. +На основе этого мы генерируем позиции для круга. + +Если бы мы остановились там, круг был бы эллипсом, +потому что clip space нормализован (идет от -1 до 1) +поперек и вниз по canvas. Если мы передадим разрешение, +мы можем учесть, что -1 до 1 поперек может не +представлять то же пространство, что и -1 до 1 вниз по canvas. + +```glsl +#version 300 es +uniform int numVerts; +uniform vec2 resolution; + +#define PI radians(180.0) + +void main() { + float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 + float angle = u * PI * 2.0; // идет от 0 до 2PI + float radius = 0.8; + + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + + float aspect = resolution.y / resolution.x; + vec2 scale = vec2(aspect, 1); + + gl_Position = vec4(pos * scale, 0, 1); + gl_PointSize = 5.0; +} +``` + +И наш фрагментный шейдер может просто рисовать сплошной цвет + +```glsl +#version 300 es +precision highp float; + +out vec4 outColor; + +void main() { + outColor = vec4(1, 0, 0, 1); +} +``` + +В нашем JavaScript во время инициализации мы скомпилируем шейдер и найдем uniforms, + +```js +// настройка GLSL программы +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const numVertsLoc = gl.getUniformLocation(program, 'numVerts'); +const resolutionLoc = gl.getUniformLocation(program, 'resolution'); +``` + +И для рендеринга мы будем использовать программу, +установим uniforms `resolution` и `numVerts`, и нарисуем точки. + +```js +gl.useProgram(program); + +const numVerts = 20; + +// сказать шейдеру количество вершин +gl.uniform1i(numVertsLoc, numVerts); +// сказать шейдеру разрешение +gl.uniform2f(resolutionLoc, gl.canvas.width, gl.canvas.height); + +const offset = 0; +gl.drawArrays(gl.POINTS, offset, numVerts); +``` + +И мы получаем круг из точек. + +{{{example url="../webgl-no-data-point-circle.html"}}} + +Полезна ли эта техника? Ну, с некоторым креативным кодом +мы могли бы сделать звездное поле или простой эффект дождя с +почти без данных и одним вызовом рисования. + +Давайте сделаем дождь, просто чтобы увидеть, как это работает. Сначала мы +изменим вершинный шейдер на + +```glsl +#version 300 es +uniform int numVerts; +uniform float time; + +void main() { + float u = float(gl_VertexID) / float(numVerts); // идет от 0 до 1 + float x = u * 2.0 - 1.0; // -1 до 1 + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 5.0; +} +``` + +Для этой ситуации нам не нужно разрешение. + +Мы добавили uniform `time`, который будет временем +в секундах с момента загрузки страницы. + +Для 'x' мы просто пойдем от -1 до 1 + +Для 'y' мы используем `time + u`, но `fract` возвращает +только дробную часть, так что значение от 0.0 до 1.0. +Расширяя это до 1.0 до -1.0, мы получаем y, который повторяется +со временем, но тот, который смещен по-разному для каждой +точки. + +Давайте изменим цвет на синий в фрагментном шейдере. + +```glsl +precision highp float; + +out vec4 outColor; + +void main() { + outColor = vec4(0, 0, 1, 1); +} +``` + +Затем в JavaScript нам нужно найти uniform времени + +```js +// настройка GLSL программы +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const numVertsLoc = gl.getUniformLocation(program, 'numVerts'); +const timeLoc = gl.getUniformLocation(program, 'time'); +``` + +И нам нужно преобразовать код в [анимацию](webgl-animation.html), +создав цикл рендеринга и установив uniform `time`. + +```js +function render(time) { + time *= 0.001; // преобразовать в секунды + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.useProgram(program); + + const numVerts = 20; + + // сказать шейдеру количество вершин + gl.uniform1i(numVertsLoc, numVerts); + // сказать шейдеру время + gl.uniform1f(timeLoc, time); + + const offset = 0; + gl.drawArrays(gl.POINTS, offset, numVerts); +} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-environment-maps.md b/webgl/lessons/ru/webgl-environment-maps.md new file mode 100644 index 000000000..4413649b0 --- /dev/null +++ b/webgl/lessons/ru/webgl-environment-maps.md @@ -0,0 +1,316 @@ +Title: WebGL2 Карты окружения (отражения) +Description: Как реализовать карты окружения. +TOC: Карты окружения + +Эта статья является частью серии статей о WebGL2. +[Первая статья начинается с основ](webgl-fundamentals.html). +Эта статья продолжается от [статьи о кубических картах](webgl-cube-maps.html). +Эта статья также использует концепции, рассмотренные в [статье об освещении](webgl-3d-lighting-directional.html). +Если вы еще не читали эти статьи, возможно, вы захотите прочитать их сначала. + +*Карта окружения* представляет окружение объектов, которые вы рисуете. +Если вы рисуете уличную сцену, она будет представлять улицу. Если +вы рисуете людей на сцене, она будет представлять место проведения. Если вы рисуете +космическую сцену, это будут звезды. Мы можем реализовать карту окружения +с кубической картой, если у нас есть 6 изображений, которые показывают окружение с точки в +пространстве в 6 направлениях кубической карты. + +Вот карта окружения из лобби Музея истории компьютеров в Маунтин-Вью, Калифорния. + +
+ + + +
+
+ + + +
+ +Основываясь на [коде в предыдущей статье](webgl-cube-maps.html), давайте загрузим эти 6 изображений вместо изображений, которые мы сгенерировали + +```js +// Создаем текстуру. +var texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); + +const faceInfos = [ + { + target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, + url: 'resources/images/computer-history-museum/pos-x.jpg', + }, + { + target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, + url: 'resources/images/computer-history-museum/neg-x.jpg', + }, + { + target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, + url: 'resources/images/computer-history-museum/pos-y.jpg', + }, + { + target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, + url: 'resources/images/computer-history-museum/neg-y.jpg', + }, + { + target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, + url: 'resources/images/computer-history-museum/pos-z.jpg', + }, + { + target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, + url: 'resources/images/computer-history-museum/neg-z.jpg', + }, +]; +faceInfos.forEach((faceInfo) => { + const {target, url} = faceInfo; + + // Загружаем холст в грань кубической карты. + const level = 0; + const internalFormat = gl.RGBA; + const width = 512; + const height = 512; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; + + // настраиваем каждую грань так, чтобы она была сразу рендерируемой + gl.texImage2D(target, level, internalFormat, width, height, 0, format, type, null); + + // Асинхронно загружаем изображение + const image = new Image(); + image.src = url; + image.addEventListener('load', function() { + // Теперь, когда изображение загружено, загружаем его в текстуру. + gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); + gl.texImage2D(target, level, internalFormat, format, type, image); + gl.generateMipmap(gl.TEXTURE_CUBE_MAP); + }); +}); +gl.generateMipmap(gl.TEXTURE_CUBE_MAP); +gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); +``` + +Обратите внимание, что для каждой грани мы инициализируем ее 512x512 пустым изображением, передавая +`null` в `texImage2D`. Кубические карты должны иметь все 6 граней, все 6 граней должны быть +одинакового размера и быть квадратными. Если они не такие, текстура не будет рендериться. Но мы +загружаем 6 изображений. Мы хотели бы начать рендеринг сразу, поэтому мы выделяем все 6 +граней, затем начинаем загружать изображения. Когда каждое изображение прибывает, мы загружаем его в +правильную грань, затем снова генерируем мипмап. Это означает, что мы можем начать рендеринг +сразу, и по мере загрузки изображений грани кубической карты будут +заполняться изображениями по одному и все еще будут рендерируемыми, даже если все 6 +еще не прибыли. + +Но просто загрузка изображений недостаточна. Как и в +[освещении](webgl-3d-lighting-point.html), нам нужна небольшая математика здесь. + +В этом случае мы хотим знать для каждого фрагмента, который нужно нарисовать, учитывая вектор от +глаза/камеры к этой позиции на поверхности объекта, в каком направлении +он отразится от этой поверхности. Мы можем затем использовать это направление, чтобы получить +цвет из кубической карты. + +Формула для отражения + + reflectionDir = eyeToSurfaceDir – + 2 ∗ dot(surfaceNormal, eyeToSurfaceDir) ∗ surfaceNormal + +Думая о том, что мы можем видеть, это правда. Напомним из [статей об освещении](webgl-3d-lighting-directional.html), +что скалярное произведение 2 векторов возвращает косинус угла между 2 +векторами. Сложение векторов дает нам новый вектор, так что давайте возьмем пример глаза, +смотрящего прямо перпендикулярно плоской поверхности. + +
+ +Давайте визуализируем формулу выше. Сначала напомним, что скалярное произведение 2 векторов, +указывающих в точно противоположных направлениях, равно -1, так что визуально + +
+ +Подставляя это скалярное произведение с eyeToSurfaceDirnormal в формулу отражения, мы получаем это + +
+ +Что умножение -2 на -1 делает его положительным 2. + +
+ +Так что сложение векторов путем их соединения дает нам отраженный вектор + +
+ +Мы можем видеть выше, что учитывая 2 нормали, одна полностью компенсирует направление от +глаза, а вторая указывает отражение прямо обратно к глазу. +Что, если мы вернем в исходную диаграмму, точно то, что мы ожидали бы + +
+ +Давайте повернем поверхность на 45 градусов вправо. + +
+ +Скалярное произведение 2 векторов на расстоянии 135 градусов равно -0.707 + +
+ +Так что подставляя все в формулу + +
+ +Снова умножение 2 отрицательных дает нам положительное, но вектор теперь примерно на 30% короче. + +
+ +Сложение векторов дает нам отраженный вектор + +
+ +Что, если мы вернем в исходную диаграмму, кажется правильным. + +
+ +Мы используем это отраженное направление, чтобы посмотреть на кубическую карту для окрашивания поверхности объекта. + +Вот диаграмма, где вы можете установить вращение поверхности и увидеть +различные части уравнения. Вы также можете увидеть, как векторы отражения указывают на +различные грани кубической карты и влияют на цвет поверхности. + +{{{diagram url="resources/environment-mapping.html" width="400" height="400" }}} + +Теперь, когда мы знаем, как работает отражение, и что мы можем использовать его для поиска значений +из кубической карты, давайте изменим шейдеры, чтобы делать это. + +Сначала в вершинном шейдере мы вычислим мировую позицию и мировую ориентированную +нормаль вершин и передадим их в фрагментный шейдер как varying. Это +похоже на то, что мы делали в [статье о прожекторах](webgl-3d-lighting-spot.html). + +```glsl +#version 300 es + +in vec4 a_position; +in vec3 a_normal; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; + +out vec3 v_worldPosition; +out vec3 v_worldNormal; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_projection * u_view * u_world * a_position; + + // передаем позицию вида в фрагментный шейдер + v_worldPosition = (u_world * a_position).xyz; + + // ориентируем нормали и передаем в фрагментный шейдер + v_worldNormal = mat3(u_world) * a_normal; +} +``` + +Затем в фрагментном шейдере мы нормализуем `worldNormal`, поскольку он +интерполируется по поверхности между вершинами. Мы передаем мировую позицию +камеры и, вычитая ее из мировой позиции поверхности, мы +получаем `eyeToSurfaceDir`. + +И наконец мы используем `reflect`, которая является встроенной функцией GLSL, реализующей +формулу, которую мы рассмотрели выше. Мы используем результат, чтобы получить цвет из +кубической карты. + +```glsl +#version 300 es + +precision highp float; + +// Передается из вершинного шейдера. +in vec3 v_worldPosition; +in vec3 v_worldNormal; + +// Текстура. +uniform samplerCube u_texture; + +// Позиция камеры +uniform vec3 u_worldCameraPosition; + +// нам нужно объявить выход для фрагментного шейдера +out vec4 outColor; + +void main() { + vec3 worldNormal = normalize(v_worldNormal); + vec3 eyeToSurfaceDir = normalize(v_worldPosition - u_worldCameraPosition); + vec3 direction = reflect(eyeToSurfaceDir,worldNormal); + + outColor = texture(u_texture, direction); +} +``` + +Нам также нужны реальные нормали для этого примера. Нам нужны реальные нормали, чтобы грани +куба выглядели плоскими. В предыдущем примере просто чтобы увидеть работу кубической карты мы +перепрофилировали позиции куба, но в этом случае нам нужны фактические нормали для +куба, как мы рассмотрели в [статье об освещении](webgl-3d-lighting-directional.html) + +Во время инициализации + +```js +// Создаем буфер для размещения нормалей +var normalBuffer = gl.createBuffer(); +// Привязываем его к ARRAY_BUFFER (думайте об этом как ARRAY_BUFFER = normalBuffer) +gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); +// Помещаем данные нормалей в буфер +setNormals(gl); + +// Говорим атрибуту, как получать данные из normalBuffer (ARRAY_BUFFER) +var size = 3; // 3 компонента на итерацию +var type = gl.FLOAT; // данные являются 32-битными значениями с плавающей точкой +var normalize = false; // нормализуем данные (конвертируем из 0-255 в 0-1) +var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой итерации, чтобы получить следующую позицию +var offset = 0; // начинаем с начала буфера +gl.vertexAttribPointer( + normalLocation, size, type, normalize, stride, offset) +``` + +И конечно нам нужно найти местоположения uniform во время инициализации + +```js +var projectionLocation = gl.getUniformLocation(program, "u_projection"); +var viewLocation = gl.getUniformLocation(program, "u_view"); +var worldLocation = gl.getUniformLocation(program, "u_world"); +var textureLocation = gl.getUniformLocation(program, "u_texture"); +var worldCameraPositionLocation = gl.getUniformLocation(program, "u_worldCameraPosition"); +``` + +и установить их во время рендеринга + +```js +// Вычисляем матрицу проекции +var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; +var projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, 1, 2000); +gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix); + +var cameraPosition = [0, 0, 2]; +var target = [0, 0, 0]; +var up = [0, 1, 0]; +// Вычисляем матрицу камеры, используя look at. +var cameraMatrix = m4.lookAt(cameraPosition, target, up); + +// Создаем матрицу вида из матрицы камеры. +var viewMatrix = m4.inverse(cameraMatrix); + +var worldMatrix = m4.xRotation(modelXRotationRadians); +worldMatrix = m4.yRotate(worldMatrix, modelYRotationRadians); + +// Устанавливаем uniform +gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix); +gl.uniformMatrix4fv(viewLocation, false, viewMatrix); +gl.uniformMatrix4fv(worldLocation, false, worldMatrix); +gl.uniform3fv(worldCameraPositionLocation, cameraPosition); + +// Говорим шейдеру использовать текстуру unit 0 для u_texture +gl.uniform1i(textureLocation, 0); +``` + +Базовые отражения + +{{{example url="../webgl-environment-map.html" }}} + +Далее давайте покажем [как использовать кубическую карту для скайбокса](webgl-skybox.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-fog.md b/webgl/lessons/ru/webgl-fog.md new file mode 100644 index 000000000..12ce912e8 --- /dev/null +++ b/webgl/lessons/ru/webgl-fog.md @@ -0,0 +1,368 @@ +Title: WebGL2 Fog +Description: Как реализовать туман +TOC: Fog + + +Эта статья является частью серии статей о WebGL. +[Первая статья начинается с основ](webgl-fundamentals.html). + +Туман в WebGL интересен мне тем, насколько *фальшивым* он кажется, когда я думаю о том, как он работает. В основном то, что вы делаете, это используете какой-то вид глубины или расстояния от камеры в ваших шейдерах, чтобы сделать цвет более или менее цветом тумана. + +Другими словами, вы начинаете с базового уравнения вроде этого + +```glsl +outColor = mix(originalColor, fogColor, fogAmount); +``` + +Где `fogAmount` - это значение от 0 до 1. Функция `mix` смешивает первые 2 значения. Когда `fogAmount` равен 0, `mix` возвращает `originalColor`. Когда `fogAmount` равен 1, `mix` возвращает `fogColor`. Между 0 и 1 вы получаете процент обоих цветов. Вы могли бы реализовать `mix` сами так + +```glsl +outColor = originalColor + (fogColor - originalColor) * fogAmount; +``` + +Давайте сделаем шейдер, который делает это. Мы будем использовать текстурированный куб из [статьи о текстурах](webgl-3d-textures.html). + +Давайте добавим смешивание в фрагментный шейдер + +```glsl +#version 300 es +precision highp float; + +// Передается из вершинного шейдера. +in vec2 v_texcoord; + +// Текстура. +uniform sampler2D u_texture; + +uniform vec4 u_fogColor; +uniform float u_fogAmount; + +out vec4 outColor; + +void main() { + vec4 color = texture(u_texture, v_texcoord); + outColor = mix(color, u_fogColor, u_fogAmount); +} +``` + +Затем во время инициализации нам нужно найти новые локации uniform + +```js +var fogColorLocation = gl.getUniformLocation(program, "u_fogColor"); +var fogAmountLocation = gl.getUniformLocation(program, "u_fogAmount"); +``` + +и во время рендеринга установить их + +```js +var fogColor = [0.8, 0.9, 1, 1]; +var settings = { + fogAmount: .5, +}; + +... + +function drawScene(time) { + ... + + // Очищаем canvas И буфер глубины. + // Очищаем до цвета тумана + gl.clearColor(...fogColor); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + ... + + // устанавливаем цвет тумана и количество + gl.uniform4fv(fogColorLocation, fogColor); + gl.uniform1f(fogAmountLocation, settings.fogAmount); + + ... +} +``` + +И вот вы увидите, если вы перетащите слайдер, вы можете изменить между текстурой и цветом тумана + +{{{example url="../webgl-3d-fog-just-mix.html" }}} + +Так что теперь все, что нам действительно нужно сделать, это вместо передачи количества тумана, мы вычисляем его на основе чего-то вроде глубины от камеры. + +Вспомните из статьи о [камерах](webgl-3d-camera.html), что после применения матрицы вида все позиции относительны к камере. Камера смотрит вниз по оси -z, поэтому если мы просто посмотрим на z позицию после умножения на мировую и видовую матрицы, мы получим значение, которое представляет, как далеко что-то находится от z плоскости камеры. + +Давайте изменим вершинный шейдер, чтобы передать эти данные в фрагментный шейдер, чтобы мы могли использовать их для вычисления количества тумана. Для этого давайте разделим `u_matrix` на 2 части. Матрицу проекции и мировую видовую матрицу. + +```glsl +#version 300 es +in vec4 a_position; +in vec2 a_texcoord; + +uniform mat4 u_worldView; +uniform mat4 u_projection; + +out vec2 v_texcoord; +out float v_fogDepth; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_projection * u_worldView * a_position; + + // Передаем texcoord в фрагментный шейдер. + v_texcoord = a_texcoord; + + // Передаем только отрицательную z позицию относительно камеры. + // камера смотрит в направлении -z, поэтому обычно вещи + // перед камерой имеют отрицательную Z позицию + // но отрицая это, мы получаем положительную глубину. + v_fogDepth = -(u_worldView * a_position).z; +} +``` + +Теперь во фрагментном шейдере мы хотим, чтобы он работал так: если глубина меньше некоторого значения, не смешивать никакой туман (fogAmount = 0). Если глубина больше некоторого значения, то 100% туман (fogAmount = 1). Между этими 2 значениями смешиваем цвета. + +Мы могли бы написать код для этого, но GLSL имеет функцию `smoothstep`, которая делает именно это. Вы даете ей минимальное значение, максимальное значение и значение для тестирования. Если тестовое значение меньше или равно минимальному значению, она возвращает 0. Если тестовое значение больше или равно максимальному значению, она возвращает 1. Если тест между этими 2 значениями, она возвращает что-то между 0 и 1 пропорционально тому, где тестовое значение находится между min и max. + +Итак, должно быть довольно легко использовать это в нашем фрагментном шейдере для вычисления количества тумана + +```glsl +#version 300 es +precision highp float; + +// Передается из вершинного шейдера. +in vec2 v_texcoord; +in float v_fogDepth; + +// Текстура. +uniform sampler2D u_texture; +uniform vec4 u_fogColor; +uniform float u_fogNear; +uniform float u_fogFar; + +out vec4 outColor; + +void main() { + vec4 color = texture(u_texture, v_texcoord); + + float fogAmount = smoothstep(u_fogNear, u_fogFar, v_fogDepth); + + outColor = mix(color, u_fogColor, fogAmount); +} +``` + +и конечно нам нужно найти все эти uniforms во время инициализации + +```js +// ищем uniforms +var projectionLocation = gl.getUniformLocation(program, "u_projection"); +var worldViewLocation = gl.getUniformLocation(program, "u_worldView"); +var textureLocation = gl.getUniformLocation(program, "u_texture"); +var fogColorLocation = gl.getUniformLocation(program, "u_fogColor"); +var fogNearLocation = gl.getUniformLocation(program, "u_fogNear"); +var fogFarLocation = gl.getUniformLocation(program, "u_fogFar"); +``` + +и установить их во время рендеринга + +```js +var fogColor = [0.8, 0.9, 1, 1]; +var settings = { + fogNear: 1.1, + fogFar: 2.0, +}; + +// Рисуем сцену. +function drawScene(time) { + ... + + // Вычисляем матрицу проекции + var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + var projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, 1, 2000); + + var cameraPosition = [0, 0, 2]; + var up = [0, 1, 0]; + var target = [0, 0, 0]; + + // Вычисляем матрицу камеры используя look at. + var cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // Делаем видовую матрицу из матрицы камеры. + var viewMatrix = m4.inverse(cameraMatrix); + + var worldViewMatrix = m4.xRotate(viewMatrix, modelXRotationRadians); + worldViewMatrix = m4.yRotate(worldViewMatrix, modelYRotationRadians); + + // Устанавливаем матрицы. + gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix); + gl.uniformMatrix4fv(worldViewLocation, false, worldViewMatrix); + + // Говорим шейдеру использовать texture unit 0 для u_texture + gl.uniform1i(textureLocation, 0); + + // устанавливаем цвет тумана и настройки near, far + gl.uniform4fv(fogColorLocation, fogColor); + gl.uniform1f(fogNearLocation, settings.fogNear); + gl.uniform1f(fogFarLocation, settings.fogFar); +} +``` + +Пока мы этим занимаемся, давайте нарисуем 40 кубов вдаль, чтобы легче было увидеть туман. + +```js +var settings = { + fogNear: 1.1, + fogFar: 2.0, + xOff: 1.1, + zOff: 1.4, +}; + +... + +const numCubes = 40; +for (let i = 0; i <= numCubes; ++i) { + var worldViewMatrix = m4.translate(viewMatrix, -2 + i * settings.xOff, 0, -i * settings.zOff); + worldViewMatrix = m4.xRotate(worldViewMatrix, modelXRotationRadians + i * 0.1); + worldViewMatrix = m4.yRotate(worldViewMatrix, modelYRotationRadians + i * 0.1); + + gl.uniformMatrix4fv(worldViewLocation, false, worldViewMatrix); + + // Рисуем геометрию. + gl.drawArrays(gl.TRIANGLES, 0, 6 * 6); +} +``` + +И теперь мы получаем туман на основе глубины + +{{{example url="../webgl-3d-fog-depth-based.html" }}} + +Примечание: Мы не добавили никакого кода, чтобы убедиться, что `fogNear` меньше или равен `fogFar`, что, возможно, недопустимые настройки, поэтому обязательно установите оба соответствующим образом. + +Как я упомянул выше, это кажется мне трюком. Это работает, потому что цвет тумана, к которому мы переходим, соответствует цвету фона. Измените цвет фона, и иллюзия исчезает. + +```js +gl.clearColor(1, 0, 0, 1); // красный +``` + +дает нам + +
+ +так что просто помните, что вам нужно установить цвет фона, чтобы он соответствовал цвету тумана. + +Использование глубины работает и это дешево, но есть проблема. Допустим, у вас есть круг объектов вокруг камеры. Мы вычисляем количество тумана на основе расстояния от z плоскости камеры. Это означает, что когда вы поворачиваете камеру, объекты будут появляться и исчезать из тумана слегка, когда их z значение в пространстве вида становится ближе к 0 + +
+ +Вы можете увидеть проблему в этом примере + +{{{example url="../webgl-3d-fog-depth-based-issue.html" }}} + +Выше есть кольцо из 8 кубов прямо вокруг камеры. Камера вращается на месте. Это означает, что кубы всегда на одинаковом расстоянии от камеры, но на разном расстоянии от z плоскости, и поэтому наш расчет количества тумана приводит к тому, что кубы у края выходят из тумана. + +Исправление заключается в том, чтобы вместо этого вычислять расстояние от камеры, которое будет одинаковым для всех кубов + +
+ +Для этого нам просто нужно передать позицию вершины в пространстве вида из вершинного шейдера в фрагментный шейдер + +```glsl +#version 300 es +in vec4 a_position; +in vec2 a_texcoord; + +uniform mat4 u_worldView; +uniform mat4 u_projection; + +out vec2 v_texcoord; +out vec3 v_position; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_projection * u_worldView * a_position; + + // Передаем texcoord в фрагментный шейдер. + v_texcoord = a_texcoord; + + // Передаем позицию вида в фрагментный шейдер + v_position = (u_worldView * a_position).xyz; +} +``` + +и затем во фрагментном шейдере мы можем использовать позицию для вычисления расстояния + +``` +#version 300 es +precision highp float; + +// Передается из вершинного шейдера. +in vec2 v_texcoord; +in vec3 v_position; + +// Текстура. +uniform sampler2D u_texture; +uniform vec4 u_fogColor; +uniform float u_fogNear; +uniform float u_fogFar; + +out vec4 outColor; + +void main() { + vec4 color = texture(u_texture, v_texcoord); + + float fogDistance = length(v_position); + float fogAmount = smoothstep(u_fogNear, u_fogFar, fogDistance); + + outColor = mix(color, u_fogColor, fogAmount); +} +``` + +И теперь кубы больше не выходят из тумана, когда камера поворачивается + +{{{example url="../webgl-3d-fog-distance-based.html" }}} + +Пока что весь наш туман использовал линейный расчет. Другими словами, цвет тумана применяется линейно между near и far. Как и многие вещи в реальном мире, туман, по-видимому, работает экспоненциально. Он становится гуще с квадратом расстояния от зрителя. Общее уравнение для экспоненциального тумана + +```glsl +#define LOG2 1.442695 + +fogAmount = 1. - exp2(-fogDensity * fogDensity * fogDistance * fogDistance * LOG2)); +fogAmount = clamp(fogAmount, 0., 1.); +``` + +Чтобы использовать это, мы изменили бы фрагментный шейдер на что-то вроде + +```glsl +#version 300 es +precision highp float; + +// Передается из вершинного шейдера. +in vec2 v_texcoord; +in vec3 v_position; + +// Текстура. +uniform sampler2D u_texture; +uniform vec4 u_fogColor; +uniform float u_fogDensity; + +out vec4 outColor; + +void main() { + vec4 color = texture(u_texture, v_texcoord); + + #define LOG2 1.442695 + + float fogDistance = length(v_position); + float fogAmount = 1. - exp2(-u_fogDensity * u_fogDensity * fogDistance * fogDistance * LOG2); + fogAmount = clamp(fogAmount, 0., 1.); + + outColor = mix(color, u_fogColor, fogAmount); +} +``` + +И мы получаем туман на основе расстояния *exp2* плотности + +{{{example url="../webgl-3d-fog-distance-exp2.html" }}} + +Одна вещь, которую стоит заметить о тумане на основе плотности, это то, что нет настроек near и far. Это может быть более реалистично, но также может не соответствовать вашим эстетическим потребностям. Какой из них вы предпочитаете - это художественное решение. + +Есть много других способов вычисления тумана. На маломощном GPU вы можете просто использовать `gl_FragCoord.z`. `gl_FragCoord` - это глобальная переменная, которую устанавливает WebGL. Компоненты `x` и `y` - это координаты пикселя, который рисуется. Компонент `z` - это глубина этого пикселя от 0 до 1. Хотя не напрямую переводится в расстояние, вы все еще можете получить что-то, что выглядит как туман, выбрав некоторые значения между 0 и 1 для near и far. Ничего не нужно передавать из вершинного шейдера в фрагментный шейдер, и никакие вычисления расстояния не нужны, так что это один способ сделать дешевый эффект тумана на маломощном GPU. + +{{{example url="../webgl-3d-fog-depth-based-gl_FragCoord.html" }}} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-framebuffers.md b/webgl/lessons/ru/webgl-framebuffers.md new file mode 100644 index 000000000..f375136d5 --- /dev/null +++ b/webgl/lessons/ru/webgl-framebuffers.md @@ -0,0 +1,133 @@ +Title: WebGL2 Framebuffers +Description: Что такое framebuffers в WebGL? +TOC: Framebuffers + +Эта статья предназначена для того, чтобы дать вам мысленный образ +того, что такое framebuffer в WebGL. Framebuffers появляются, +когда они позволяют вам [рендерить в текстуру](webgl-render-to-texture.html). + +Framebuffer это просто *коллекция attachments*. Вот и все! Он +используется для того, чтобы позволить рендеринг в текстуры и renderbuffers. + +Вы можете думать об объекте Framebuffer так + +``` +class Framebuffer { + constructor() { + this.attachments = new Map(); // attachments по attachment point + this.drawBuffers = [gl.BACK, gl.NONE, gl.NONE, gl.NONE, ...]; + this.readBuffer = gl.BACK, + } +} +``` + +И `WebGL2RenderingContext` (объект `gl`) так + +```js +// псевдо код +gl = { + drawFramebufferBinding: defaultFramebufferForCanvas, + readFramebufferBinding: defaultFramebufferForCanvas, +} +``` + +Есть 2 binding points. Они устанавливаются так + +```js +gl.bindFramebuffer(target, framebuffer) { + framebuffer = framebuffer || defaultFramebufferForCanvas; // если null используем canvas + switch (target) { + case: gl.DRAW_FRAMEBUFFER: + this.drawFramebufferBinding = framebuffer; + break; + case: gl.READ_FRAMEBUFFER: + this.readFramebufferBinding = framebuffer; + break; + case: gl.FRAMEBUFFER: + this.drawFramebufferBinding = framebuffer; + this.readFramebufferBinding = framebuffer; + break; + default: + ... error ... + } +} +``` + +`DRAW_FRAMEBUFFER` binding используется при рисовании в framebuffer через `gl.clear`, `gl.draw???`, или `gl.blitFramebuffer`. +`READ_FRAMEBUFFER` binding используется при чтении из framebuffer через `gl.readPixels` или `gl.blitFramebuffer`. + +Вы можете добавлять attachments к framebuffer через 3 функции, `framebufferTexture2D`, +`framebufferRenderbuffer`, и `framebufferTextureLayer`. + +Мы можем представить их реализацию чем-то вроде + +```js +// псевдо код +gl._getFramebufferByTarget(target) { + switch (target) { + case gl.FRAMEBUFFER: + case gl.DRAW_FRAMEBUFFER: + return this.drawFramebufferBinding; + case gl.READ_FRAMEBUFFER: + return this.readFramebufferBinding; + } +} +gl.framebufferTexture2D(target, attachmentPoint, texTarget, texture, mipLevel) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + texture, texTarget, mipLevel, + }); +} +gl.framebufferTextureLayer(target, attachmentPoint, texture, mipLevel, layer) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + texture, texTarget, mipLevel, layer + }); +} +gl.framebufferRenderbuffer(target, attachmentPoint, renderbufferTarget, renderbuffer) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + renderbufferTarget, renderbuffer + }); +} +``` + +Вы можете установить массив drawing buffer с `gl.drawBuffers`, который мы можем +представить реализованным так + +```js +// псевдо код +gl.drawBuffers(drawBuffers) { + const framebuffer = this._getFramebufferByTarget(gl.DRAW_FRAMEBUFFER); + for (let i = 0; i < maxDrawBuffers; ++i) { + framebuffer.drawBuffers[i] = i < drawBuffers.length + ? drawBuffers[i] + : gl.NONE + } +} +``` + +Массив drawBuffers определяет, рендерится ли в конкретный attachment или нет. +Допустимые значения либо `gl.NONE`, что означает *не рендерить в этот attachment*, либо +`gl.COLOR_ATTACHMENTx`, где `x` то же самое, что и индекс attachment. Еще одно +значение это `gl.BACK`, которое допустимо только когда `null` привязан к текущему framebuffer, +в этом случае `gl.BACK` означает 'рендерить в backbuffer (canvas)' + +Вы можете установить read buffer с `gl.readBuffer` + +```js +// псевдо код +gl.readBuffer(readBuffer) { + const framebuffer = this._getFramebufferByTarget(gl.READ_FRAMEBUFFER); + framebuffer.readBuffer = readBuffer; +} +``` + +readBuffer устанавливает, какой attachment читается при вызове `gl.readPixels`. + +Важная часть в том, что *framebuffer* это просто простая коллекция attachments. +Сложности в ограничениях на то, чем могут быть эти attachments +и какие комбинации работают. Например, attachment с floating point текстурой +не может быть отрендерен по умолчанию. Расширения могут включить это, такие как +`EXT_color_buffer_float`. Аналогично, если есть +больше одного attachment, они все должны быть одинаковых размеров. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-fundamentals.md b/webgl/lessons/ru/webgl-fundamentals.md new file mode 100644 index 000000000..9c66748a0 --- /dev/null +++ b/webgl/lessons/ru/webgl-fundamentals.md @@ -0,0 +1,200 @@ +Title: Основы WebGL2 +Description: Ваш первый урок WebGL2, начинающийся с основ +TOC: Основы + + +Прежде всего, эти статьи посвящены WebGL2. Если вас интересует WebGL 1.0, +[пожалуйста, перейдите сюда](https://webglfundamentals.org). Обратите внимание, что WebGL2 [почти на 100% обратно +совместим с WebGL 1](webgl1-to-webgl2.html). Тем не менее, как только вы включите +WebGL2, вам стоит использовать его так, как он был задуман. Эти туториалы следуют +этому пути. + +WebGL часто рассматривается как 3D API. Люди думают "Я использую WebGL и *магия* я получу крутую 3D графику". +В реальности WebGL - это просто движок растеризации. Он рисует [точки, линии и треугольники](webgl-points-lines-triangles.html) на основе +кода, который вы предоставляете. Заставить WebGL делать что-то еще - ваша задача предоставить код для использования точек, линий +и треугольников для выполнения вашей задачи. + +WebGL работает на GPU вашего компьютера. Как таковой, вам нужно предоставить код, который работает на этом GPU. +Вы предоставляете этот код в виде пар функций. Эти 2 функции называются вершинным шейдером +и фрагментным шейдером, и каждая из них написана на очень строго типизированном языке, похожем на C/C++, называемом +[GLSL](webgl-shaders-and-glsl.html). (GL Shader Language). Вместе они называются *программой*. + +Задача вершинного шейдера - вычислять позиции вершин. На основе позиций, которые выводит функция, +WebGL может затем растеризовать различные виды примитивов, включая [точки, линии или треугольники](webgl-points-lines-triangles.html). +При растеризации этих примитивов он вызывает вторую пользовательскую функцию, называемую фрагментным шейдером. +Задача фрагментного шейдера - вычислять цвет для каждого пикселя примитива, который в данный момент рисуется. + +Почти весь WebGL API посвящен [настройке состояния](resources/webgl-state-diagram.html) для выполнения этих пар функций. +Для каждой вещи, которую вы хотите нарисовать, вы настраиваете кучу состояния, а затем выполняете пару функций, вызывая +`gl.drawArrays` или `gl.drawElements`, который выполняет ваши шейдеры на GPU. + +Любые данные, к которым вы хотите, чтобы эти функции имели доступ, должны быть предоставлены GPU. Есть 4 способа, +как шейдер может получать данные. + +1. Атрибуты, буферы и вершинные массивы + + Буферы - это массивы бинарных данных, которые вы загружаете в GPU. Обычно буферы содержат + такие вещи, как позиции, нормали, координаты текстуры, цвета вершин и т.д., хотя + вы можете положить в них все, что хотите. + + Атрибуты используются для указания того, как + извлекать данные из ваших буферов и предоставлять их вашему вершинному шейдеру. + Например, вы можете положить позиции в буфер как три 32-битных float'а + на позицию. Вы бы сказали конкретному атрибуту, из какого буфера извлекать позиции, какой тип + данных он должен извлекать (3 компонента 32-битных чисел с плавающей точкой), какое смещение + в буфере начинаются позиции, и сколько байт нужно получить от одной позиции до следующей. + + Буферы не являются случайным доступом. Вместо этого вершинный шейдер выполняется указанное количество + раз. Каждый раз, когда он выполняется, извлекается следующее значение из каждого указанного буфера + и присваивается атрибуту. + + Состояние атрибутов, какие буферы использовать для каждого из них, и как извлекать данные + из этих буферов собирается в объект вершинного массива (VAO). + +2. Uniforms + + Uniforms - это эффективно глобальные переменные, которые вы устанавливаете перед выполнением вашей шейдерной программы. + +3. Текстуры + + Текстуры - это массивы данных, к которым вы можете получить случайный доступ в вашей шейдерной программе. Самая + распространенная вещь, которую кладут в текстуру - это данные изображения, но текстуры - это просто данные и могут + так же легко содержать что-то другое, кроме цветов. + +4. Varyings + + Varyings - это способ для вершинного шейдера передать данные фрагментному шейдеру. В зависимости + от того, что рендерится, точки, линии или треугольники, значения, установленные на varying + вершинным шейдером, будут интерполированы при выполнении фрагментного шейдера. + +## WebGL Hello World + +WebGL заботится только о 2 вещах. Координаты clip space и цвета. +Ваша задача как программиста, использующего WebGL, - предоставить WebGL эти 2 вещи. +Вы предоставляете ваши 2 "шейдера" для этого. Вершинный шейдер, который предоставляет +координаты clip space, и фрагментный шейдер, который предоставляет цвет. + +Координаты clip space всегда идут от -1 до +1 независимо от размера вашего +canvas. Вот простой пример WebGL, который показывает WebGL в его простейшей форме. + +Давайте начнем с вершинного шейдера + + #version 300 es + + // атрибут - это вход (in) в вершинный шейдер. + // Он будет получать данные из буфера + in vec4 a_position; + + // все шейдеры имеют главную функцию + void main() { + + // gl_Position - это специальная переменная, за установку которой + // отвечает вершинный шейдер + gl_Position = a_position; + } + +При выполнении, если бы вся вещь была написана в JavaScript вместо GLSL, +вы могли бы представить, что она использовалась бы так + + // *** ПСЕВДО КОД!! *** + + var positionBuffer = [ + 0, 0, 0, 0, + 0, 0.5, 0, 0, + 0.7, 0, 0, 0, + ]; + var attributes = {}; + var gl_Position; + + drawArrays(..., offset, count) { + var stride = 4; + var size = 4; + for (var i = 0; i < count; ++i) { + // копируем следующие 4 значения из positionBuffer в атрибут a_position + const start = offset + i * stride; + attributes.a_position = positionBuffer.slice(start, start + size); + runVertexShader(); + ... + doSomethingWith_gl_Position(); + } + +В реальности это не совсем так просто, потому что `positionBuffer` нужно было бы преобразовать в бинарные +данные (см. ниже), и поэтому фактическое вычисление для получения данных из буфера +было бы немного другим, но надеюсь, это дает вам представление о том, как вершинный +шейдер будет выполняться. + +Далее нам нужен фрагментный шейдер + + #version 300 es + + // фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно + // выбрать одну. highp - хороший выбор по умолчанию. Это означает "высокая точность" + precision highp float; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + // Просто устанавливаем выход на константный красно-фиолетовый + outColor = vec4(1, 0, 0.5, 1); + } + +Выше мы объявили `outColor` как выход нашего фрагментного шейдера. Мы устанавливаем `outColor` в `1, 0, 0.5, 1`, +что означает 1 для красного, 0 для зеленого, 0.5 для синего, 1 для альфа. Цвета в WebGL идут от 0 до 1. + +Теперь, когда мы написали 2 шейдерные функции, давайте начнем с WebGL + +Сначала нам нужен HTML canvas элемент + + + +Затем в JavaScript мы можем найти его + + var canvas = document.querySelector("#c"); + +Теперь мы можем создать WebGL2RenderingContext + + var gl = canvas.getContext("webgl2"); + if (!gl) { + // нет webgl2 для вас! + ... + +Теперь нам нужно скомпилировать эти шейдеры, чтобы поместить их на GPU, поэтому сначала нам нужно получить их в строки. +Вы можете создавать ваши GLSL строки любым способом, которым вы обычно создаете строки в JavaScript. Например, конкатенацией, +используя AJAX для их загрузки, помещая их в не-javascript script теги, или в данном случае в +многострочные шаблонные строки. + + var vertexShaderSource = `#version 300 es + + // атрибут - это вход (in) в вершинный шейдер. + // Он будет получать данные из буфера + in vec4 a_position; + + // все шейдеры имеют главную функцию + void main() { + + // gl_Position - это специальная переменная, за установку которой + // отвечает вершинный шейдер + gl_Position = a_position; + } + `; + + var fragmentShaderSource = `#version 300 es + + // фрагментные шейдеры не имеют точности по умолчанию, поэтому нам нужно + // выбрать одну. highp - хороший выбор по умолчанию. Это означает "высокая точность" + precision highp float; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + // Просто устанавливаем выход на константный красно-фиолетовый + outColor = vec4(1, 0, 0.5, 1); + } + `; + +Фактически, большинство 3D движков генерируют GLSL шейдеры на лету, используя различные типы шаблонов, конкатенацию и т.д. +Для примеров на этом сайте, однако, ни один из них не достаточно сложен, чтобы нуждаться в генерации GLSL во время выполнения. + +> ПРИМЕЧАНИЕ: `#version 300 es` **ДОЛЖНА БЫТЬ САМОЙ ПЕРВОЙ СТРОКОЙ ВАШЕГО ШЕЙДЕРА**. Никаких комментариев или \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-getting-webgl2.md b/webgl/lessons/ru/webgl-getting-webgl2.md new file mode 100644 index 000000000..6a71d4693 --- /dev/null +++ b/webgl/lessons/ru/webgl-getting-webgl2.md @@ -0,0 +1,8 @@ +Title: Как использовать WebGL2 +Description: Получение браузера с поддержкой WebGL2 +TOC: Как использовать WebGL2 + +По состоянию на сентябрь 2021 года, WebGL2 доступен в последних версиях Chrome, Edge, +Firefox, Safari и Opera. + +Обратите внимание, что в Safari WebGL2 появился только в Safari 15 в сентябре 2021 года. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-gpgpu.md b/webgl/lessons/ru/webgl-gpgpu.md new file mode 100644 index 000000000..78abc6fd0 --- /dev/null +++ b/webgl/lessons/ru/webgl-gpgpu.md @@ -0,0 +1,983 @@ +Title: WebGL2 GPGPU +Description: Как выполнять общие вычисления с помощью WebGL +TOC: GPGPU + +GPGPU означает "General Purpose" GPU и означает использование GPU для чего-то +другого, кроме рисования пикселей. + +Основное понимание для осознания GPGPU в WebGL заключается в том, что текстура +- это не изображение, а 2D массив значений. В [статье о текстурах](webgl-3d-textures.html) +мы рассмотрели чтение из текстуры. В [статье о рендеринге в текстуру](webgl-render-to-texture.html) +мы рассмотрели запись в текстуру. Итак, если осознать, что текстура - это 2D массив значений, +мы можем сказать, что мы действительно описали способ чтения из и записи в 2D массивы. +Аналогично буфер - это не просто позиции, нормали, координаты текстуры и цвета. +Эти данные могут быть чем угодно. Скорости, массы, цены акций и т.д. +Творческое использование этих знаний для выполнения математики - это суть GPGPU в WebGL. + +## Сначала сделаем это с текстурами + +В JavaScript есть функция [`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), которая для данного массива вызывает функцию для каждого элемента + +```js +function multBy2(v) { + return v * 2; +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = src.map(multBy2); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +Вы можете рассматривать `multBy2` как шейдер, а `map` как аналогичный вызову `gl.drawArrays` или `gl.drawElements`. +Некоторые различия. + +## Шейдеры не генерируют новый массив, вы должны предоставить его + +Мы можем симулировать это, создав собственную функцию map + +```js +function multBy2(v) { + return v * 2; +} + +function mapSrcToDst(src, fn, dst) { + for (let i = 0; i < src.length; ++i) { + dst[i] = fn(src[i]); + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // чтобы симулировать, что в WebGL мы должны выделить текстуру +mapSrcToDst(src, multBy2, dst); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +## Шейдеры не возвращают значение, они устанавливают переменную `out` + +Это довольно легко симулировать + +```js +let outColor; + +function multBy2(v) { + outColor = v * 2; +} + +function mapSrcToDst(src, fn, dst) { + for (let i = 0; i < src.length; ++i) { + fn(src[i]); + dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // чтобы симулировать, что в WebGL мы должны выделить текстуру +mapSrcToDst(src, multBy2, dst); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +## Шейдеры основаны на назначении, а не на источнике. + +Другими словами, они перебирают назначение и спрашивают "какое значение я должен положить сюда" + +```js +let outColor; + +function multBy2(src) { + return function(i) { + outColor = src[i] * 2; + } +} + +function mapDst(dst, fn) { + for (let i = 0; i < dst.length; ++i) { + fn(i); + dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // чтобы симулировать, что в WebGL мы должны выделить текстуру +mapDst(dst, multBy2(src)); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +## В WebGL индекс или ID пикселя, значение которого вас просят предоставить, называется `gl_FragCoord` + +```js +let outColor; +let gl_FragCoord; + +function multBy2(src) { + return function() { + outColor = src[gl_FragCoord] * 2; + } +} + +function mapDst(dst, fn) { + for (let i = 0; i < dst.length; ++i) { + gl_FragCoord = i; + fn(); + dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // чтобы симулировать, что в WebGL мы должны выделить текстуру +mapDst(dst, multBy2(src)); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +## В WebGL текстуры - это 2D массивы. + +Давайте предположим, что наш массив `dst` представляет текстуру 3x2 + +```js +let outColor; +let gl_FragCoord; + +function multBy2(src, across) { + return function() { + outColor = src[gl_FragCoord.y * across + gl_FragCoord.x] * 2; + } +} + +function mapDst(dst, across, up, fn) { + for (let y = 0; y < up; ++y) { + for (let x = 0; x < across; ++x) { + gl_FragCoord = {x, y}; + fn(); + dst[y * across + x] = outColor; + } + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // чтобы симулировать, что в WebGL мы должны выделить текстуру +mapDst(dst, 3, 2, multBy2(src, 3)); + +// dst теперь [2, 4, 6, 8, 10, 12]; +``` + +И мы могли бы продолжать. Я надеюсь, что примеры выше помогают вам увидеть, что GPGPU в WebGL +довольно прост концептуально. Давайте действительно сделаем вышесказанное в WebGL. + +Для понимания следующего кода вам нужно будет, как минимум, прочитать +[статью об основах](webgl-fundamentals.html), вероятно, статью о +[Как это работает](webgl-how-it-works.html), статью о [GLSL](webgl-shaders-and-glsl.html) +и [статью о текстурах](webgl-3d-textures.html). + +```js +const vs = `#version 300 es +in vec4 position; +void main() { + gl_Position = position; +} +`; + +const fs = `#version 300 es +precision highp float; + +uniform sampler2D srcTex; + +out vec4 outColor; + +void main() { + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + vec4 value = texelFetch(srcTex, texelCoord, 0); // 0 = mip level 0 + outColor = value * 2.0; +} +`; + +const dstWidth = 3; +const dstHeight = 2; + +// создаем canvas 3x2 для 6 результатов +const canvas = document.createElement('canvas'); +canvas.width = dstWidth; +canvas.height = dstHeight; + +const gl = canvas.getContext('webgl2'); + +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const positionLoc = gl.getAttribLocation(program, 'position'); +const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); + +// настраиваем полноэкранный quad в clip space +const buffer = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, -1, + 1, -1, + -1, 1, + -1, 1, + 1, -1, + 1, 1, +]), gl.STATIC_DRAW); + +// Создаем объект вершинного массива (состояние атрибутов) +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); + +// настраиваем наши атрибуты, чтобы сказать WebGL, как извлекать +// данные из буфера выше в атрибут position +gl.enableVertexAttribArray(positionLoc); +gl.vertexAttribPointer( + positionLoc, + 2, // размер (количество компонентов) + gl.FLOAT, // тип данных в буфере + false, // нормализовать + 0, // шаг (0 = авто) + 0, // смещение +); + +// создаем нашу исходную текстуру +const srcWidth = 3; +const srcHeight = 2; +const tex = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, tex); +gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); // см. https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html +gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip уровень + gl.R8, // внутренний формат + srcWidth, + srcHeight, + 0, // граница + gl.RED, // формат + gl.UNSIGNED_BYTE, // тип + new Uint8Array([ + 1, 2, 3, + 4, 5, 6, + ])); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + +gl.useProgram(program); +gl.uniform1i(srcTexLoc, 0); // говорим шейдеру, что исходная текстура находится на texture unit 0 + +gl.drawArrays(gl.TRIANGLES, 0, 6); // рисуем 2 треугольника (6 вершин) + +// получаем результат +const results = new Uint8Array(dstWidth * dstHeight * 4); +gl.readPixels(0, 0, dstWidth, dstHeight, gl.RGBA, gl.UNSIGNED_BYTE, results); + +// выводим результаты +for (let i = 0; i < dstWidth * dstHeight; ++i) { + log(results[i * 4]); +} +``` + +и вот он работает + +{{{example url="../webgl-gpgpu-mult-by-2.html"}}} + +Некоторые замечания о коде выше. + +* Мы рисуем quad в clip space от -1 до +1. + + Мы создаем вершины для quad от -1 до +1 из 2 треугольников. Это означает, что при правильной настройке viewport + мы нарисуем все пиксели в назначении. Другими словами, мы попросим + наш шейдер сгенерировать значение для каждого элемента в результирующем массиве. Этот массив в + данном случае - это сам canvas. + +* `texelFetch` - это функция текстуры, которая ищет один texel из текстуры. + + Она принимает 3 параметра. Сэмплер, координаты texel на основе целых чисел, и mip уровень. + `gl_FragCoord` - это vec2, нам нужно превратить его в `ivec2`, чтобы использовать с + `texelFetch`. Здесь нет дополнительной математики, пока исходная текстура и + текстура назначения имеют одинаковый размер, что в данном случае так и есть. + +* Наш шейдер записывает 4 значения на пиксель + + В данном конкретном случае это влияет на то, как мы читаем вывод. Мы просим `RGBA/UNSIGNED_BYTE` + из `readPixels` [потому что другие комбинации формата/типа не поддерживаются](webgl-readpixels.html). + Поэтому нам нужно смотреть на каждое 4-е значение для нашего ответа. + + Примечание: Было бы умно попытаться воспользоваться тем фактом, что WebGL делает 4 значения за раз + для еще большей скорости. + +* Мы используем `R8` как внутренний формат нашей текстуры. + + Это означает, что только красный канал из текстуры имеет значение из наших данных. + +* И наши входные данные, и выходные данные (canvas) - это значения `UNSIGNED_BYTE` + + Это означает, что мы можем передавать и получать обратно только целые значения от 0 до 255. + Мы могли бы использовать разные форматы для ввода, предоставляя текстуру другого формата. + Мы также могли бы попытаться рендерить в текстуру другого формата для большего диапазона выходных значений. + +В примере выше src и dst имеют одинаковый размер. Давайте изменим это так, чтобы мы добавляли каждые 2 значения +из src, чтобы сделать dst. Другими словами, учитывая `[1, 2, 3, 4, 5, 6]` как ввод, мы хотим +`[3, 7, 11]` как вывод. И далее, давайте сохраним источник как данные 3x2 + +Основная формула для получения значения из 2D массива, как если бы это был 1D массив + +```js +y = floor(indexInto1DArray / widthOf2DArray); +x = indexInto1DArray % widthOf2Array; +``` + +Учитывая это, наш фрагментный шейдер должен измениться на это, чтобы добавить каждые 2 значения. + +```glsl +#version 300 es +precision highp float; + +uniform sampler2D srcTex; +uniform ivec2 dstDimensions; + +out vec4 outColor; + +vec4 getValueFrom2DTextureAs1DArray(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); +} + +void main() { + // вычисляем 1D индекс в dst + ivec2 dstPixel = ivec2(gl_FragCoord.xy); + int dstIndex = dstPixel.y * dstDimensions.x + dstPixel.x; + + ivec2 srcDimensions = textureSize(srcTex, 0); // размер mip 0 + + vec4 v1 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2); + vec4 v2 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2 + 1); + + outColor = v1 + v2; +} +``` + +Функция `getValueFrom2DTextureAs1DArray` - это в основном наша функция доступа к массиву. +Это означает, что эти 2 строки + +```glsl + vec4 v1 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2.0); + vec4 v2 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2.0 + 1.0); +``` + +Эффективно означают это + +```glsl + vec4 v1 = srcTexAs1DArray[dstIndex * 2.0]; + vec4 v2 = setTexAs1DArray[dstIndex * 2.0 + 1.0]; +``` + +В нашем JavaScript нам нужно найти местоположение `dstDimensions` + +```js +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const positionLoc = gl.getAttribLocation(program, 'position'); +const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); ++const dstDimensionsLoc = gl.getUniformLocation(program, 'dstDimensions'); +``` + +и установить его + +```js +gl.useProgram(program); +gl.uniform1i(srcTexLoc, 0); // говорим шейдеру, что исходная текстура находится на texture unit 0 ++gl.uniform2f(dstDimensionsLoc, dstWidth, dstHeight); +``` + +и нам нужно изменить размер назначения (canvas) + +```js +const dstWidth = 3; +-const dstHeight = 2; ++const dstHeight = 1; +``` + +и с этим у нас теперь есть результирующий массив, способный выполнять математику +со случайным доступом в исходный массив + +{{{example url="../webgl-gpgpu-add-2-elements.html"}}} + +Если вы хотели бы использовать больше массивов как ввод, просто добавьте больше текстур, чтобы поместить больше +данных в ту же текстуру. + +## Теперь сделаем это с *transform feedback* + +"Transform Feedback" - это модное название для способности записывать вывод +varyings в вершинном шейдере в один или несколько буферов. + +Преимущество использования transform feedback в том, что вывод одномерный, +поэтому, вероятно, легче рассуждать об этом. Это даже ближе к `map` из JavaScript. + +Давайте возьмем 2 массива значений и выведем их сумму, разность +и произведение. Вот вершинный шейдер + +```glsl +#version 300 es + +in float a; +in float b; + +out float sum; +out float difference; +out float product; + +void main() { + sum = a + b; + difference = a - b; + product = a * b; +} +``` + +и фрагментный шейдер просто достаточно для компиляции + +```glsl +#version 300 es +precision highp float; +void main() { +} +``` + +Чтобы использовать transform feedback, мы должны сказать WebGL, какие varyings мы хотим записать +и в каком порядке. Мы делаем это, вызывая `gl.transformFeedbackVaryings` перед +линковкой шейдерной программы. Из-за этого мы не будем использовать наш помощник +для компиляции шейдеров и линковки программы на этот раз, просто чтобы было ясно, +что мы должны сделать. + +Итак, вот код для компиляции шейдера, аналогичный коду в самой +[первой статье](webgl-fundamentals.html). + +```js +function createShader(gl, type, src) { + const shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(gl.getShaderInfoLog(shader)); + } + return shader; +} +``` + +Мы будем использовать его для компиляции наших 2 шейдеров, а затем прикрепить их и вызвать +`gl.transformFeedbackVaryings` перед линковкой + +```js +const vShader = createShader(gl, gl.VERTEX_SHADER, vs); +const fShader = createShader(gl, gl.FRAGMENT_SHADER, fs); + +const program = gl.createProgram(); +gl.attachShader(program, vShader); +gl.attachShader(program, fShader); +gl.transformFeedbackVaryings( + program, + ['sum', 'difference', 'product'], + gl.SEPARATE_ATTRIBS, +); +gl.linkProgram(program); +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(gl.getProgramParameter(program)); +} +``` + +`gl.transformFeedbackVaryings` принимает 3 аргумента. Программу, массив имен +varyings, которые мы хотим записать в том порядке, в котором вы хотите их записать. +Если бы у вас был фрагментный шейдер, который действительно что-то делал, +то, возможно, некоторые из ваших varyings предназначены только для фрагментного шейдера и поэтому +не нуждаются в записи. В нашем случае мы запишем все наши varyings, поэтому передаем +имена всех 3. Последний параметр может быть одним из 2 значений. Либо `SEPARATE_ATTRIBS`, +либо `INTERLEAVED_ATTRIBS`. + +`SEPARATE_ATTRIBS` означает, что каждый varying будет записан в другой буфер. +`INTERLEAVED_ATTRIBS` означает, что все varyings будут записаны в тот же буфер, +но перемежаться в том порядке, который мы указали. В нашем случае, поскольку мы указали +`['sum', 'difference', 'product']`, если бы мы использовали `INTERLEAVED_ATTRIBS`, вывод +был бы `sum0, difference0, product0, sum1, difference1, product1, sum2, difference2, product2, etc...` +в один буфер. Мы используем `SEPARATE_ATTRIBS`, поэтому вместо этого +каждый вывод будет записан в другой буфер. + +Итак, как и в других примерах, нам нужно настроить буферы для наших входных атрибутов + +```js +const aLoc = gl.getAttribLocation(program, 'a'); +const bLoc = gl.getAttribLocation(program, 'b'); + +// Создаем объект вершинного массива (состояние атрибутов) +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); + +function makeBuffer(gl, sizeOrData) { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, gl.STATIC_DRAW); + return buf; +} + +function makeBufferAndSetAttribute(gl, data, loc) { + const buf = makeBuffer(gl, data); + // настраиваем наши атрибуты, чтобы сказать WebGL, как извлекать + // данные из буфера выше в атрибут + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer( + loc, + 1, // размер (количество компонентов) + gl.FLOAT, // тип данных в буфере + false, // нормализовать + 0, // шаг (0 = авто) + 0, // смещение + ); +} + +const a = [1, 2, 3, 4, 5, 6]; +const b = [3, 6, 9, 12, 15, 18]; + +// помещаем данные в буферы +const aBuffer = makeBufferAndSetAttribute(gl, new Float32Array(a), aLoc); +const bBuffer = makeBufferAndSetAttribute(gl, new Float32Array(b), bLoc); +``` + +Затем нам нужно настроить "transform feedback". "Transform feedback" - это объект, +который содержит состояние буферов, в которые мы будем записывать. В то время как [вершинный массив](webgl-attributes.html) +указывает состояние всех входных атрибутов, "transform feedback" содержит +состояние всех выходных атрибутов. + +Вот код для настройки нашего + +```js +// Создаем и заполняем transform feedback +const tf = gl.createTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + +// создаем буферы для вывода +const sumBuffer = makeBuffer(gl, a.length * 4); +const differenceBuffer = makeBuffer(gl, a.length * 4); +const productBuffer = makeBuffer(gl, a.length * 4); + +// привязываем буферы к transform feedback +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, sumBuffer); +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, differenceBuffer); +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, productBuffer); + +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + +// буферы, в которые мы записываем, не могут быть привязаны где-то еще +gl.bindBuffer(gl.ARRAY_BUFFER, null); // productBuffer все еще был привязан к ARRAY_BUFFER, поэтому отвязываем его +``` + +Мы вызываем `bindBufferBase`, чтобы установить, в какой буфер каждый из выходов, выход 0, выход 1 и выход 2 +будет записывать. Выходы 0, 1, 2 соответствуют именам, которые мы передали в `gl.transformFeedbackVaryings` +когда мы линковали программу. + +Когда мы закончили, "transform feedback", который мы создали, имеет состояние, как это + + + +Есть также функция `bindBufferRange`, которая позволяет нам указать поддиапазон в буфере, где +мы будем записывать, но мы не будем использовать это здесь. + +Итак, чтобы выполнить шейдер, мы делаем это + +```js +gl.useProgram(program); + +// привязываем наше состояние входных атрибутов для буферов a и b +gl.bindVertexArray(vao); + +// нет необходимости вызывать фрагментный шейдер +gl.enable(gl.RASTERIZER_DISCARD); + +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); +gl.beginTransformFeedback(gl.POINTS); +gl.drawArrays(gl.POINTS, 0, a.length); +gl.endTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + +// включаем использование фрагментных шейдеров снова +gl.disable(gl.RASTERIZER_DISCARD); +``` + +Мы отключаем вызов фрагментного шейдера. Мы привязываем объект transform feedback, +который мы создали ранее, мы включаем transform feedback, затем мы вызываем draw. + +Чтобы посмотреть на значения, мы можем вызвать `gl.getBufferSubData` + +```js +log(`a: ${a}`); +log(`b: ${b}`); + +printResults(gl, sumBuffer, 'sums'); +printResults(gl, differenceBuffer, 'differences'); +printResults(gl, productBuffer, 'products'); + +function printResults(gl, buffer, label) { + const results = new Float32Array(a.length); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.getBufferSubData( + gl.ARRAY_BUFFER, + 0, // смещение в байтах в GPU буфере, + results, + ); + // выводим результаты + log(`${label}: ${results}`); +} +``` + +{{{example url="../webgl-gpgpu-sum-difference-product-transformfeedback.html"}}} + +Вы можете видеть, что это сработало. Мы заставили GPU вычислить сумму, разность и произведение +значений 'a' и 'b', которые мы передали. + +Примечание: Вы можете найти [этот пример диаграммы состояния transform feedback](https://webgl2fundamentals.org/webgl/lessons/resources/webgl-state-diagram.html?exampleId=transform-feedback) полезным для визуализации того, что такое "transform feedback". +Это не тот же пример, что выше, хотя. Вершинный шейдер, который он использует с transform feedback, генерирует позиции и цвета для круга точек. + +## Первый пример: частицы + +Допустим, у вас есть очень простая система частиц. +Каждая частица просто имеет позицию и скорость, и +если она выходит за один край экрана, она оборачивается вокруг +другой стороны. + +Учитывая большинство других статей на этом сайте, вы бы +обновляли позиции частиц в JavaScript + +```js +for (const particle of particles) { + particle.pos.x = (particle.pos.x + particle.velocity.x) % canvas.width; + particle.pos.y = (particle.pos.y + particle.velocity.y) % canvas.height; +} +``` + +и затем рисовали бы частицы либо по одной за раз + +``` +useProgram (particleShader) +setup particle attributes +for each particle + set uniforms + draw particle +``` + +Или вы могли бы загрузить все новые позиции частиц + +``` +bindBuffer(..., particlePositionBuffer) +bufferData(..., latestParticlePositions, ...) +useProgram (particleShader) +setup particle attributes +set uniforms +draw particles +``` + +Используя пример transform feedback выше, мы могли бы создать +буфер со скоростью для каждой частицы. Затем мы могли бы +создать 2 буфера для позиций. Мы использовали бы transform feedback +для добавления скорости к одному буферу позиций и записи в +другой буфер позиций. Затем мы рисовали бы с новыми позициями. +На следующем кадре мы читали бы из буфера с новыми позициями +и записывали обратно в другой буфер для генерации еще более новых позиций. + +Вот вершинный шейдер для обновления позиций частиц + +```glsl +#version 300 es +in vec2 oldPosition; +in vec2 velocity; + +uniform float deltaTime; +uniform vec2 canvasDimensions; + +out vec2 newPosition; + +vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); +} + +void main() { + newPosition = euclideanModulo( + oldPosition + velocity * deltaTime, + canvasDimensions); +} +``` + +Чтобы рисовать частицы, мы просто используем простой вершинный шейдер + +```glsl +#version 300 es +in vec4 position; +uniform mat4 matrix; + +void main() { + // делаем общую матричную математику + gl_Position = matrix * position; + gl_PointSize = 10.0; +} +``` + +Давайте превратим код для создания и линковки программы в +функцию, которую мы можем использовать для обоих шейдеров + +```js +function createProgram(gl, shaderSources, transformFeedbackVaryings) { + const program = gl.createProgram(); + [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => { + const shader = createShader(gl, type, shaderSources[ndx]); + gl.attachShader(program, shader); + }); + if (transformFeedbackVaryings) { + gl.transformFeedbackVaryings( + program, + transformFeedbackVaryings, + gl.SEPARATE_ATTRIBS, + ); + } + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(gl.getProgramParameter(program)); + } + return program; +} +``` + +и затем использовать его для компиляции шейдеров, один с transform feedback +varying. + +```js +const updatePositionProgram = createProgram( + gl, [updatePositionVS, updatePositionFS], ['newPosition']); +const drawParticlesProgram = createProgram( + gl, [drawParticlesVS, drawParticlesFS]); +``` + +Как обычно, нам нужно найти местоположения + +```js +const updatePositionPrgLocs = { + oldPosition: gl.getAttribLocation(updatePositionProgram, 'oldPosition'), + velocity: gl.getAttribLocation(updatePositionProgram, 'velocity'), + canvasDimensions: gl.getUniformLocation(updatePositionProgram, 'canvasDimensions'), + deltaTime: gl.getUniformLocation(updatePositionProgram, 'deltaTime'), +}; + +const drawParticlesProgLocs = { + position: gl.getAttribLocation(drawParticlesProgram, 'position'), + matrix: gl.getUniformLocation(drawParticlesProgram, 'matrix'), +}; +``` + +Теперь давайте создадим некоторые случайные позиции и скорости + +```js +// создаем случайные позиции и скорости. +const rand = (min, max) => { + if (max === undefined) { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; +}; +const numParticles = 200; +const createPoints = (num, ranges) => + new Array(num).fill(0).map(_ => ranges.map(range => rand(...range))).flat(); +const positions = new Float32Array(createPoints(numParticles, [[canvas.width], [canvas.height]])); +const velocities = new Float32Array(createPoints(numParticles, [[-300, 300], [-300, 300]])); +``` + +Затем мы поместим их в буферы. + +```js +function makeBuffer(gl, sizeOrData, usage) { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage); + return buf; +} + +const position1Buffer = makeBuffer(gl, positions, gl.DYNAMIC_DRAW); +const position2Buffer = makeBuffer(gl, positions, gl.DYNAMIC_DRAW); +const velocityBuffer = makeBuffer(gl, velocities, gl.STATIC_DRAW); +``` + +Обратите внимание, что мы передали `gl.DYNAMIC_DRAW` в `gl.bufferData` для 2 буферов позиций, +поскольку мы будем обновлять их часто. Это просто подсказка для WebGL для оптимизации. +Имеет ли это какой-либо эффект на производительность, зависит от WebGL. + +Нам нужно 4 вершинных массива. + +* 1 для использования `position1Buffer` и `velocity` при обновлении позиций +* 1 для использования `position2Buffer` и `velocity` при обновлении позиций +* 1 для использования `position1Buffer` при рисовании +* 1 для использования `position2Buffer` при рисовании + +```js +function makeVertexArray(gl, bufLocPairs) { + const va = gl.createVertexArray(); + gl.bindVertexArray(va); + for (const [buffer, loc] of bufLocPairs) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer( + loc, // местоположение атрибута + 2, // количество элементов + gl.FLOAT, // тип данных + false, // нормализовать + 0, // шаг (0 = авто) + 0, // смещение + ); + } + return va; +} + +const updatePositionVA1 = makeVertexArray(gl, [ + [position1Buffer, updatePositionPrgLocs.oldPosition], + [velocityBuffer, updatePositionPrgLocs.velocity], +]); +const updatePositionVA2 = makeVertexArray(gl, [ + [position2Buffer, updatePositionPrgLocs.oldPosition], + [velocityBuffer, updatePositionPrgLocs.velocity], +]); + +const drawVA1 = makeVertexArray( + gl, [[position1Buffer, drawParticlesProgLocs.position]]); +const drawVA2 = makeVertexArray( + gl, [[position2Buffer, drawParticlesProgLocs.position]]); +``` + +Затем мы создаем 2 объекта transform feedback. + +* 1 для записи в `position1Buffer` +* 1 для записи в `position2Buffer` + +```js +function makeTransformFeedback(gl, buffer) { + const tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + return tf; +} + +const tf1 = makeTransformFeedback(gl, position1Buffer); +const tf2 = makeTransformFeedback(gl, position2Buffer); +``` + +При использовании transform feedback важно отвязать буферы +от других точек привязки. `ARRAY_BUFFER` все еще имеет последний буфер +привязанным, в который мы поместили данные. `TRANSFORM_FEEDBACK_BUFFER` устанавливается при +вызове `gl.bindBufferBase`. Это немного запутанно. Вызов +`gl.bindBufferBase` с `TRANSFORM_FEEDBACK_BUFFER` фактически +привязывает буфер к 2 местам. Одно - к индексированной точке привязки внутри +объекта transform feedback. Другое - к своего рода глобальной +точке привязки, называемой `TRANSFORM_FEEDBACK_BUFFER`. + +```js +// отвязываем оставшиеся вещи +gl.bindBuffer(gl.ARRAY_BUFFER, null); +gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null); +``` + +Чтобы мы могли легко менять местами буферы обновления и рисования, +мы настроим эти 2 объекта + +```js +let current = { + updateVA: updatePositionVA1, // читаем из position1 + tf: tf2, // записываем в position2 + drawVA: drawVA2, // рисуем с position2 +}; +let next = { + updateVA: updatePositionVA2, // читаем из position2 + tf: tf1, // записываем в position1 + drawVA: drawVA1, // рисуем с position1 +}; +``` + +Затем мы сделаем цикл рендеринга, сначала мы обновим позиции +используя transform feedback. + +```js +let then = 0; +function render(time) { + // конвертируем в секунды + time *= 0.001; + // Вычитаем предыдущее время из текущего времени + const deltaTime = time - then; + // Запоминаем текущее время для следующего кадра. + then = time; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + gl.clear(gl.COLOR_BUFFER_BIT); + + // вычисляем новые позиции + gl.useProgram(updatePositionProgram); + gl.bindVertexArray(current.updateVA); + gl.uniform2f(updatePositionPrgLocs.canvasDimensions, gl.canvas.width, gl.canvas.height); + gl.uniform1f(updatePositionPrgLocs.deltaTime, deltaTime); + + // отключаем использование фрагментного шейдера + gl.enable(gl.RASTERIZER_DISCARD); + + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, current.tf); + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, numParticles); + gl.endTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + + // включаем использование фрагментных шейдеров снова + gl.disable(gl.RASTERIZER_DISCARD); +``` + +и затем рисуем частицы + +```js + // теперь рисуем частицы. + gl.useProgram(drawParticlesProgram); + gl.bindVertexArray(current.drawVA); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.uniformMatrix4fv( + drawParticlesProgLocs.matrix, + false, + m4.orthographic(0, gl.canvas.width, 0, gl.canvas.height, -1, 1)); + gl.drawArrays(gl.POINTS, 0, numParticles); +``` + +и наконец меняем местами `current` и `next`, чтобы на следующем кадре мы +использовали последние позиции для генерации новых + +```js + // меняем местами, из какого буфера мы будем читать + // и в какой мы будем записывать + { + const temp = current; + current = next; + next = temp; + } + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +И с этим у нас есть простые частицы на основе GPU. + +{{{example url="../webgl-gpgpu-particles-transformfeedback.html"}}} + +## Следующий пример: Поиск ближайшего отрезка линии к точке + +Я не уверен, что это хороший пример, но это тот, который я написал. Я говорю, что он может +быть нехорошим, потому что я подозреваю, что есть лучшие алгоритмы для поиска +ближайшей линии к точке, чем перебор проверки каждой линии с точкой. Например, различные алгоритмы пространственного разделения могут позволить вам легко отбросить 95% +точек и поэтому быть быстрее. Тем не менее, этот пример, вероятно, показывает +некоторые техники GPGPU по крайней мере. + +Проблема: У нас есть 500 точек и 1000 отрезков линий. Для каждой точки +найти, какой отрезок линии к ней ближе всего. Метод перебора + +``` +for each point + minDistanceSoFar = MAX_VALUE + for each line segment + compute distance from point to line segment +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-how-it-works.md b/webgl/lessons/ru/webgl-how-it-works.md new file mode 100644 index 000000000..aa9e3ed68 --- /dev/null +++ b/webgl/lessons/ru/webgl-how-it-works.md @@ -0,0 +1,200 @@ +Title: Как работает WebGL2 +Description: Что WebGL на самом деле делает под капотом +TOC: Как это работает + + +Это продолжение [Основ WebGL](webgl-fundamentals.html). +Прежде чем мы продолжим, я думаю, нам нужно обсудить на +базовом уровне, что WebGL и ваш GPU на самом деле делают. Есть в основном 2 +части в этой GPU штуке. Первая часть обрабатывает вершины (или потоки +данных) в вершины clip space. Вторая часть рисует пиксели на основе +первой части. + +Когда вы вызываете + + gl.drawArrays(gl.TRIANGLES, 0, 9); + +9 там означает "обработать 9 вершин", так что вот 9 вершин обрабатываются. + +
+ +Слева данные, которые вы предоставляете. Вершинный шейдер - это функция, которую вы +пишете на [GLSL](webgl-shaders-and-glsl.html). Она вызывается один раз для каждой вершины. +Вы делаете некоторую математику и устанавливаете специальную переменную `gl_Position` значением clip space +для текущей вершины. GPU берет это значение и сохраняет его внутренне. + +Предполагая, что вы рисуете `TRIANGLES`, каждый раз, когда эта первая часть генерирует 3 +вершины, GPU использует их для создания треугольника. Он выясняет, каким +пикселям соответствуют 3 точки треугольника, а затем растеризует +треугольник, что является модным словом для "рисует его пикселями". Для каждого +пикселя он вызовет ваш фрагментный шейдер, спрашивая, какой цвет сделать для этого +пикселя. Ваш фрагментный шейдер выводит vec4 +с цветом, который он хочет для этого пикселя. + +Это все очень интересно, но как вы можете видеть в наших примерах до +этого момента фрагментный шейдер имеет очень мало информации на пиксель. +К счастью, мы можем передать ему больше информации. Мы определяем "varyings" для каждого +значения, которое мы хотим передать от вершинного шейдера к фрагментному шейдеру. + +Как простой пример, давайте просто передадим координаты clip space, которые мы вычислили +напрямую от вершинного шейдера к фрагментному шейдеру. + +Мы будем рисовать простым треугольником. Продолжая с нашего +[предыдущего примера](webgl-2d-matrices.html), давайте изменим наш прямоугольник на +треугольник. + + // Заполняем буфер значениями, которые определяют треугольник. + function setGeometry(gl) { + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 0, -100, + 150, 125, + -175, 100]), + gl.STATIC_DRAW); + } + +И нам нужно рисовать только 3 вершины. + + // Рисуем сцену. + function drawScene() { + ... + // Рисуем геометрию. + * gl.drawArrays(gl.TRIANGLES, 0, 3); + } + +Затем в нашем вершинном шейдере мы объявляем *varying*, делая `out` для передачи данных в +фрагментный шейдер. + + out vec4 v_color; + ... + void main() { + // Умножаем позицию на матрицу. + gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); + + // Преобразуем из clip space в цветовое пространство. + // Clip space идет от -1.0 до +1.0 + // Цветовое пространство идет от 0.0 до 1.0 + * v_color = gl_Position * 0.5 + 0.5; + } + +И затем мы объявляем тот же *varying* как `in` в фрагментном шейдере. + + #version 300 es + + precision highp float; + + in vec4 v_color; + + out vec4 outColor; + + void main() { + * outColor = v_color; + } + +WebGL соединит varying в вершинном шейдере с varying +того же имени и типа в фрагментном шейдере. + +Вот рабочая версия. + +{{{example url="../webgl-2d-triangle-with-position-for-color.html" }}} + +Перемещайте, масштабируйте и поворачивайте треугольник. Обратите внимание, что поскольку цвета +вычисляются из clip space, они не двигаются с треугольником. Они +относительны к фону. + +Теперь подумайте об этом. Мы вычисляем только 3 вершины. Наш вершинный шейдер +вызывается только 3 раза, поэтому он вычисляет только 3 цвета, но наш +треугольник имеет много цветов. Вот почему это называется *varying*. + +WebGL берет 3 значения, которые мы вычислили для каждой вершины, и когда он растеризует +треугольник, он интерполирует между значениями, которые мы вычислили для +вершин. Для каждого пикселя он вызывает наш фрагментный шейдер с +интерполированным значением для этого пикселя. + +В примере выше мы начинаем с 3 вершин + + +
+ + + + + +
Вершины
0-100
150125
-175100
+
+ +Наш вершинный шейдер применяет матрицу для перемещения, поворота, масштабирования и преобразования +в clip space. Значения по умолчанию для перемещения, поворота и масштабирования: +перемещение = 200, 150, поворот = 0, масштаб = 1,1, так что это действительно только +перемещение. Учитывая, что наш backbuffer 400x300, наш вершинный шейдер применяет +матрицу и затем вычисляет следующие 3 вершины clip space. + +
+ + + + + +
значения, записанные в gl_Position
0.0000.660
0.750-0.830
-0.875-0.660
+
+ +Он также преобразует их в цветовое пространство и записывает их в *varying* +v_color, который мы объявили. + +
+ + + + + +
значения, записанные в v_color
0.50000.8300.5
0.87500.0860.5
0.06250.1700.5
+
+ +Те 3 значения, записанные в v_color, затем интерполируются и передаются в +фрагментный шейдер для каждого пикселя. + +{{{diagram url="resources/fragment-shader-anim.html" width="600" height="400" caption="v_color интерполируется между v0, v1 и v2" }}} + +Мы также можем передать больше данных в вершинный шейдер, которые мы можем затем передать +в фрагментный шейдер. Так, например, давайте нарисуем прямоугольник, который +состоит из 2 треугольников, в 2 цветах. Для этого мы добавим еще один +атрибут в вершинный шейдер, чтобы мы могли передать ему больше данных, и мы передадим +эти данные напрямую в фрагментный шейдер. + + in vec2 a_position; + +in vec4 a_color; + ... + out vec4 v_color; + + void main() { + ... + // Копируем цвет из атрибута в varying. + * v_color = a_color; + } + +Теперь нам нужно предоставить цвета для WebGL. + + // ищем, куда должны идти данные вершин. + var positionLocation = gl.getAttribLocation(program, "a_position"); + + var colorLocation = gl.getAttribLocation(program, "a_color"); \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-image-processing-continued.md b/webgl/lessons/ru/webgl-image-processing-continued.md new file mode 100644 index 000000000..344cacbdc --- /dev/null +++ b/webgl/lessons/ru/webgl-image-processing-continued.md @@ -0,0 +1,198 @@ +Title: WebGL2 Продвинутая обработка изображений +Description: Как применять несколько техник обработки изображений в WebGL +TOC: Продвинутая обработка изображений + + +Эта статья является продолжением [WebGL Обработка изображений](webgl-image-processing.html). +Если вы не читали её, советую [начать с неё](webgl-image-processing.html). + +Следующий очевидный вопрос для обработки изображений — как применить несколько эффектов? + +Можно попробовать генерировать шейдеры на лету. Сделать UI, который позволит +пользователю выбрать нужные эффекты, а затем сгенерировать шейдер, который выполнит +все эти эффекты. Это не всегда возможно, хотя такой подход часто используется для +[создания эффектов в реальном времени](https://www.youtube.com/watch?v=cQUn0Zeh-0Q). + +Более гибкий способ — использовать ещё 2 *рабочие* текстуры и +рендерить поочередно в каждую из них, чередуя (ping-pong), +и каждый раз применять следующий эффект. + +
Оригинальное изображение -> [Blur]        -> Текстура 1
+Текстура 1              -> [Sharpen]     -> Текстура 2
+Текстура 2              -> [Edge Detect] -> Текстура 1
+Текстура 1              -> [Blur]        -> Текстура 2
+Текстура 2              -> [Normal]      -> Canvas
+ +Для этого нам нужно создать framebuffer'ы. В WebGL и OpenGL framebuffer — не совсем удачное название. На самом деле framebuffer — это просто +список привязок (attachments), а не какой-то буфер. Но, привязывая текстуру к framebuffer'у, мы можем рендерить в эту текстуру. + +Сначала превратим [старый код создания текстуры](webgl-image-processing.html) в функцию: + +``` + function createAndSetupTexture(gl) { + 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); + + return texture; + } + + // Создаём текстуру и кладём в неё изображение. + var originalImageTexture = createAndSetupTexture(gl); + + // Загружаем изображение в текстуру. + var mipLevel = 0; // самый крупный mip + var internalFormat = gl.RGBA; // формат, который хотим в текстуре + var srcFormat = gl.RGBA; // формат исходных данных + var srcType = gl.UNSIGNED_BYTE // тип исходных данных + gl.texImage2D(gl.TEXTURE_2D, + mipLevel, + internalFormat, + srcFormat, + srcType, + image); +``` + +Теперь используем эту функцию, чтобы создать ещё 2 текстуры и привязать их к 2 framebuffer'ам. + +``` + // создаём 2 текстуры и привязываем их к framebuffer'ам. + var textures = []; + var framebuffers = []; + for (var ii = 0; ii < 2; ++ii) { + var texture = createAndSetupTexture(gl); + textures.push(texture); + + // делаем текстуру такого же размера, как изображение + var mipLevel = 0; // самый крупный mip + var internalFormat = gl.RGBA; // формат, который хотим в текстуре + var border = 0; // должен быть 0 + var srcFormat = gl.RGBA; // формат исходных данных + var srcType = gl.UNSIGNED_BYTE // тип исходных данных + var data = null; // нет данных = создаём пустую текстуру + gl.texImage2D( + gl.TEXTURE_2D, mipLevel, internalFormat, image.width, image.height, border, + srcFormat, srcType, data); + + // Создаём framebuffer + var fbo = gl.createFramebuffer(); + framebuffers.push(fbo); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + // Привязываем к нему текстуру. + var attachmentPoint = gl.COLOR_ATTACHMENT0; + gl.framebufferTexture2D( + gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, texture, mipLevel); + } +``` + +Теперь создадим набор ядер (kernels), а затем список, какие из них применять. + +``` + // Определяем несколько свёрточных ядер + var kernels = { + normal: [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ], + gaussianBlur: [ + 0.045, 0.122, 0.045, + 0.122, 0.332, 0.122, + 0.045, 0.122, 0.045 + ], + unsharpen: [ + -1, -1, -1, + -1, 9, -1, + -1, -1, -1 + ], + emboss: [ + -2, -1, 0, + -1, 1, 1, + 0, 1, 2 + ] + }; + + // Список эффектов, которые нужно применить. + var effectsToApply = [ + "gaussianBlur", + "emboss", + "gaussianBlur", + "unsharpen" + ]; +``` + +И наконец применим каждый из них, чередуя, в какую текстуру рендерим + +``` + function drawEffects() { + // Говорим использовать нашу программу (пару шейдеров) + gl.useProgram(program); + + // Привязываем нужный набор атрибутов/буферов. + gl.bindVertexArray(vao); + + // начинаем с оригинального изображения на unit 0 + gl.activeTexture(gl.TEXTURE0 + 0); + gl.bindTexture(gl.TEXTURE_2D, originalImageTexture); + + // Говорим шейдеру брать текстуру из texture unit 0 + gl.uniform1i(imageLocation, 0); + + // не переворачиваем изображение по Y при рендере в текстуры + gl.uniform1f(flipYLocation, 1); + + // проходим по каждому эффекту, который хотим применить. + var count = 0; + for (var ii = 0; ii < tbody.rows.length; ++ii) { + var checkbox = tbody.rows[ii].firstChild.firstChild; + if (checkbox.checked) { + // Настраиваем рендер в один из framebuffer'ов. + setFramebuffer(framebuffers[count % 2], image.width, image.height); + + drawWithKernel(checkbox.value); + + // для следующего эффекта используем текстуру, в которую только что отрендерили. + gl.bindTexture(gl.TEXTURE_2D, textures[count % 2]); + + // увеличиваем count, чтобы в следующий раз использовать другую текстуру. + ++count; + } + } + + // наконец рендерим результат на canvas. + gl.uniform1f(flipYLocation, -1); // нужно перевернуть по Y для canvas + + setFramebuffer(null, gl.canvas.width, gl.canvas.height); + + drawWithKernel("normal"); + } + + function setFramebuffer(fbo, width, height) { + // делаем этот framebuffer текущим для рендера. + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + // Говорим шейдеру разрешение framebuffer'а. + gl.uniform2f(resolutionLocation, width, height); + + // Говорим WebGL, как преобразовывать из clip space в пиксели + gl.viewport(0, 0, width, height); + } + + function drawWithKernel(name) { + // задаём ядро и его вес + gl.uniform1fv(kernelLocation, kernels[name]); + gl.uniform1f(kernelWeightLocation, computeKernelWeight(kernels[name])); + + // Рисуем прямоугольник. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 6; + gl.drawArrays(primitiveType, offset, count); + } +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-image-processing.md b/webgl/lessons/ru/webgl-image-processing.md new file mode 100644 index 000000000..5665f0090 --- /dev/null +++ b/webgl/lessons/ru/webgl-image-processing.md @@ -0,0 +1,194 @@ +Title: WebGL2 Обработка изображений +Description: Как обрабатывать изображения в WebGL +TOC: Обработка изображений + + +Обработка изображений в WebGL — это просто. Насколько просто? Читайте ниже. + +Это продолжение [WebGL2 Основы](webgl-fundamentals.html). +Если вы не читали её, советую [начать с неё](webgl-fundamentals.html). + +Чтобы рисовать изображения в WebGL, нам нужно использовать текстуры. Аналогично тому, +как WebGL ожидает координаты clip space при рендеринге вместо пикселей, +WebGL обычно ожидает координаты текстуры при чтении текстуры. +Координаты текстуры идут от 0.0 до 1.0 независимо от размеров текстуры. + +WebGL2 добавляет возможность читать текстуру с помощью пиксельных координат. +Какой способ лучше — решать вам. Мне кажется, чаще используют +координаты текстуры, чем пиксельные координаты. + +Поскольку мы рисуем всего один прямоугольник (точнее, 2 треугольника), +нам нужно сообщить WebGL, какое место в текстуре соответствует каждой точке +прямоугольника. Мы передадим эту информацию из вершинного шейдера во фрагментный +с помощью специальной переменной, называемой 'varying'. Она так называется, +потому что её значение меняется. [WebGL будет интерполировать значения](webgl-how-it-works.html), +которые мы задаём в вершинном шейдере, когда будет рисовать каждый пиксель во фрагментном шейдере. + +Используя [вершинный шейдер из конца предыдущей статьи](webgl-fundamentals.html), +нам нужно добавить атрибут для передачи координат текстуры и затем передать их во фрагментный шейдер. + + ... + + +in vec2 a_texCoord; + + ... + + +out vec2 v_texCoord; + + void main() { + ... + + // передаём texCoord во фрагментный шейдер + + // GPU будет интерполировать это значение между точками + + v_texCoord = a_texCoord; + } + +Теперь напишем фрагментный шейдер, который берёт цвет из текстуры. + + #version 300 es + precision highp float; + + // наша текстура + uniform sampler2D u_image; + + // координаты текстуры, переданные из вершинного шейдера + in vec2 v_texCoord; + + // объявляем выход для фрагментного шейдера + out vec4 outColor; + + void main() { + // Берём цвет из текстуры + outColor = texture(u_image, v_texCoord); + } + +Далее нам нужно загрузить изображение, создать текстуру и скопировать изображение +в текстуру. Поскольку мы в браузере, изображения загружаются асинхронно, +поэтому нужно немного изменить код, чтобы дождаться загрузки текстуры. +Когда она загрузится, мы нарисуем её. + + +function main() { + + var image = new Image(); + + image.src = "https://someimage/on/our/server"; // ДОЛЖНО БЫТЬ НА ТОМ ЖЕ ДОМЕНЕ!!! + + image.onload = function() { + + render(image); + + } + +} + + function render(image) { + ... + // получаем, куда нужно положить данные вершин + var positionAttributeLocation = gl.getAttribLocation(program, "a_position"); + + var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord"); + + // получаем uniform'ы + var resolutionLocation = gl.getUniformLocation(program, "u_resolution"); + + var imageLocation = gl.getUniformLocation(program, "u_image"); + + ... + + + // задаём координаты текстуры для прямоугольника + + var texCoordBuffer = gl.createBuffer(); + + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + + 0.0, 0.0, + + 1.0, 0.0, + + 0.0, 1.0, + + 0.0, 1.0, + + 1.0, 0.0, + + 1.0, 1.0]), gl.STATIC_DRAW); + + gl.enableVertexAttribArray(texCoordAttributeLocation); + + var size = 2; // 2 компонента на итерацию + + var type = gl.FLOAT; // данные — 32-битные float'ы + + var normalize = false; // не нормализуем данные + + var stride = 0; // 0 = переходить на size * sizeof(type) байт для следующей позиции + + var offset = 0; // начинать с начала буфера + + gl.vertexAttribPointer( + + texCoordAttributeLocation, size, type, normalize, stride, offset) + + + + // Создаём текстуру. + + var texture = gl.createTexture(); + + + + // делаем unit 0 активным текстурным юнитом + + // (т.е. все команды текстур будут влиять на него) + + gl.activeTexture(gl.TEXTURE0 + 0); + + + + // Привязываем текстуру к 2D bind point текстурного юнита 0 + + gl.bindTexture(gl.TEXTURE_2D, texture); + + + + // Задаём параметры, чтобы не было mip-уровней, не было фильтрации + + // и не было повторения + + 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; // самый крупный mip + + var internalFormat = gl.RGBA; // формат, который хотим в текстуре + + var srcFormat = gl.RGBA; // формат исходных данных + + var srcType = gl.UNSIGNED_BYTE // тип исходных данных + + gl.texImage2D(gl.TEXTURE_2D, + + mipLevel, + + internalFormat, + + srcFormat, + + srcType, + + image); + + ... + + // Говорим использовать нашу программу (пару шейдеров) + gl.useProgram(program); + + // Передаём разрешение canvas, чтобы можно было преобразовать + // пиксели в clip space в шейдере + gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); + + + // Говорим шейдеру брать текстуру из texture unit 0 + + gl.uniform1i(imageLocation, 0); + + + // Привязываем буфер позиций, чтобы gl.bufferData, который будет вызван + + // в setRectangle, положил данные в буфер позиций + + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + + + + // Задаём прямоугольник такого же размера, как изображение. + + setRectangle(gl, 0, 0, image.width, image.height); + + } + +Вот изображение, отрендеренное в WebGL. + +{{{example url="../webgl-2d-image.html" }}} + +Пока не очень интересно, давайте изменим изображение. Например, поменяем местами красный и синий: + + ... + outColor = texture(u_image, v_texCoord).bgra; + ... + +Теперь красный и синий поменялись местами. + +{{{example url="../webgl-2d-image-red2blue.html" }}} + +А если мы хотим обработку, которая смотрит на соседние пиксели? Поскольку WebGL оперирует координатами текстуры от 0.0 до 1.0, мы можем вычислить смещение на 1 пиксель так: onePixel = 1.0 / textureSize. + +Вот фрагментный шейдер, который усредняет левый и правый пиксели для каждого пикселя текстуры: + +``` +#version 300 es + +// фрагментные шейдеры не имеют точности по умолчанию, поэтому нужно +// выбрать одну. highp — хороший выбор по умолчанию. Это "высокая точность" +precision highp float; + +// наша текстура +uniform sampler2D u_image; + +// координаты текстуры, переданные из вершинного шейдера +in vec2 v_texCoord; + +// объявляем выход для фрагментного шейдера +out vec4 outColor; + +void main() { ++ vec2 onePixel = vec2(1) / vec2(textureSize(u_image, 0)); +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-indexed-vertices.md b/webgl/lessons/ru/webgl-indexed-vertices.md new file mode 100644 index 000000000..11c6faa0b --- /dev/null +++ b/webgl/lessons/ru/webgl-indexed-vertices.md @@ -0,0 +1,122 @@ +Title: WebGL2 Индексированные вершины +Description: Как использовать gl.drawElements +TOC: Индексированные вершины (gl.drawElements) + +Эта статья предполагает, что вы хотя бы прочитали +[статью об основах](webgl-fundamentals.html). Если +вы ещё не читали её, лучше начните с неё. + +Это короткая статья о `gl.drawElements`. В WebGL есть 2 +базовые функции отрисовки: `gl.drawArrays` и `gl.drawElements`. +В большинстве статей на сайте, где явно вызывается одна из них, +используется `gl.drawArrays`, так как она наиболее прямолинейна. + +`gl.drawElements`, с другой стороны, использует буфер с +индексами вершин и рисует на его основе. + +Давайте возьмём пример, который рисует прямоугольники из +[первой статьи](webgl-fundamentals.html), и сделаем его с использованием +`gl.drawElements`. + +В том коде мы создавали прямоугольник из 2 треугольников, по 3 вершины +в каждом, всего 6 вершин. + +Вот код, который задаёт 6 позиций вершин: + +```js + var x1 = x; + var x2 = x + width; + var y1 = y; + var y2 = y + height; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + x1, y1, // вершина 0 + x2, y1, // вершина 1 + x1, y2, // вершина 2 + x1, y2, // вершина 3 + x2, y1, // вершина 4 + x2, y2, // вершина 5 + ]), gl.STATIC_DRAW); +``` + +Вместо этого мы можем использовать данные для 4 вершин: + +```js + var x1 = x; + var x2 = x + width; + var y1 = y; + var y2 = y + height; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + x1, y1, // вершина 0 + x2, y1, // вершина 1 + x1, y2, // вершина 2 + x2, y2, // вершина 3 + ]), gl.STATIC_DRAW); +``` + +Но тогда нам нужно добавить ещё один буфер с индексами, потому что WebGL всё равно +требует, чтобы для отрисовки 2 треугольников мы указали 6 вершин в сумме. + +Для этого мы создаём ещё один буфер, но используем другую точку привязки. +Вместо `ARRAY_BUFFER` используем `ELEMENT_ARRAY_BUFFER`, который всегда используется для индексов. + +```js +// создаём буфер +const indexBuffer = gl.createBuffer(); + +// делаем этот буфер текущим 'ELEMENT_ARRAY_BUFFER' +gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); + +// Заполняем текущий element array buffer данными +const indices = [ + 0, 1, 2, // первый треугольник + 2, 1, 3, // второй треугольник +]; +gl.bufferData( + gl.ELEMENT_ARRAY_BUFFER, + new Uint16Array(indices), + gl.STATIC_DRAW +); +``` + +Как и все данные в WebGL, индексы должны быть в определённом формате. +Мы преобразуем индексы в беззнаковые 16-битные числа с помощью +`new Uint16Array(indices)` и загружаем их в буфер. + +Важно отметить, что в отличие от точки привязки `ARRAY_BUFFER`, +которая является глобальным состоянием, точка привязки `ELEMENT_ARRAY_BUFFER` +является частью текущего vertex array. + +В коде мы создавали и привязывали vertex array, а затем настраивали +буфер индексов. Это значит, что, как и атрибуты, каждый раз при +привязке этого vertex array буфер индексов тоже будет привязан. +Подробнее см. [статью об атрибутах](webgl-attributes.html). + +При отрисовке вызываем `drawElements`: + +```js +// Рисуем прямоугольник. +var primitiveType = gl.TRIANGLES; +var offset = 0; +var count = 6; +-gl.drawArrays(primitiveType, offset, count); ++var indexType = gl.UNSIGNED_SHORT; ++gl.drawElements(primitiveType, count, indexType, offset); +``` + +Результат тот же, что и раньше, но теперь мы задали данные только для 4 +вершин вместо 6. Всё равно пришлось попросить WebGL нарисовать 6 вершин, но +это позволило переиспользовать данные 4 вершин через индексы. + +{{{example url="../webgl-2d-rectangles-indexed.html"}}} + +Использовать индексированные или неиндексированные данные — решать вам. + +Важно: индексированные вершины обычно не позволят сделать куб с 8 позициями вершин, +потому что обычно вы хотите ассоциировать с каждой вершиной дополнительные данные, +которые различаются в зависимости от того, с какой гранью используется эта вершина. +Например, если вы хотите дать каждой грани куба свой цвет, нужно передать этот цвет вместе с позицией. Поэтому, даже если одна и та же позиция используется 3 раза (по одной на каждую грань, к которой вершина принадлежит), всё равно нужно повторить позицию 3 раза — по одной на каждую грань, с разным цветом. Это значит, что для куба потребуется 24 вершины (по 4 на каждую грань) и 36 индексов для 12 треугольников. + +Допустимые типы для `indexType` выше: `gl.UNSIGNED_BYTE` (индексы от 0 до 255, используйте `new Uint8Array(indices)`), +`gl.UNSIGNED_SHORT` (максимальный индекс 65535, как выше), +и `gl.UNSIGNED_INT` (максимальный индекс 4294967296, используйте +`new Uint32Array(indices)`). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-instanced-drawing.md b/webgl/lessons/ru/webgl-instanced-drawing.md new file mode 100644 index 000000000..9f4000e22 --- /dev/null +++ b/webgl/lessons/ru/webgl-instanced-drawing.md @@ -0,0 +1,298 @@ +Title: WebGL2 Оптимизация — Инстансинг (Instanced Drawing) +Description: Рисование нескольких экземпляров одного объекта +TOC: Инстансинг (Instanced Drawing) + +В WebGL есть возможность, называемая *инстансинг* (instanced drawing). +Это способ нарисовать несколько одинаковых объектов быстрее, чем рисовать каждый по отдельности. + +Для начала сделаем пример, который рисует несколько экземпляров одного и того же объекта. + +Начнём с кода, *похожего* на тот, что был в конце +[статьи про ортографическую проекцию](webgl-3d-orthographic.html): + +```js +const vertexShaderSource = `#version 300 es +in vec4 a_position; +uniform mat4 matrix; + +void main() { + // Умножаем позицию на матрицу + gl_Position = matrix * a_position; +} +`; + +const fragmentShaderSource = `#version 300 es +precision highp float; + +uniform vec4 color; + +out vec4 outColor; + +void main() { + outColor = color; +} +`; +``` + +Вершинный шейдер умножает каждую вершину на одну матрицу (см. +[ту статью](webgl-3d-orthographic.html)), что довольно гибко. Фрагментный шейдер просто использует +цвет, который мы передаём через uniform. + +Чтобы рисовать, нужно скомпилировать шейдеры, связать их вместе +и получить локации атрибутов и uniform'ов. + +```js +const program = webglUtils.createProgramFromSources(gl, + [vertexShaderSource, fragmentShaderSource]); + +const positionLoc = gl.getAttribLocation(program, 'a_position'); +const colorLoc = gl.getUniformLocation(program, 'color'); +const matrixLoc = gl.getUniformLocation(program, 'matrix'); +``` + +Создаём vertex array object для хранения состояния атрибутов: + +```js +// Создаём vertex array object (состояние атрибутов) +const vao = gl.createVertexArray(); + +// и делаем его активным +gl.bindVertexArray(vao); +``` + +Далее нужно передать данные позиций через буфер. + +```js +const positionBuffer = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -0.1, 0.4, + -0.1, -0.4, + 0.1, -0.4, + -0.1, 0.4, + 0.1, -0.4, + 0.1, 0.4, + -0.4, -0.1, + 0.4, -0.1, + -0.4, 0.1, + -0.4, 0.1, + 0.4, -0.1, + 0.4, 0.1, + ]), gl.STATIC_DRAW); +const numVertices = 12; + +// настраиваем атрибут позиции +gl.enableVertexAttribArray(positionLoc); +gl.vertexAttribPointer( + positionLoc, // location + 2, // размер (сколько значений брать из буфера за итерацию) + gl.FLOAT, // тип данных в буфере + false, // нормализовать + 0, // шаг (0 = вычислять из size и type выше) + 0, // смещение в буфере +); +``` + +Нарисуем 5 экземпляров. Сделаем 5 матриц и 5 цветов для каждого экземпляра. + +```js +const numInstances = 5; +const matrices = [ + m4.identity(), + m4.identity(), + m4.identity(), + m4.identity(), + m4.identity(), +]; + +const colors = [ + [ 1, 0, 0, 1, ], // красный + [ 0, 1, 0, 1, ], // зелёный + [ 0, 0, 1, 1, ], // синий + [ 1, 0, 1, 1, ], // маджента + [ 0, 1, 1, 1, ], // циан +]; +``` + +Для отрисовки используем шейдерную программу, настраиваем атрибуты, +и затем в цикле по 5 экземплярам вычисляем новую матрицу для каждого, +устанавливаем uniform'ы матрицы и цвета, и рисуем. + +```js +function render(time) { + time *= 0.001; // секунды + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + // Говорим WebGL, как преобразовывать из clip space в пиксели + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.useProgram(program); + + // настраиваем все атрибуты + gl.bindVertexArray(vao); + + matrices.forEach((mat, ndx) => { + m4.translation(-0.5 + ndx * 0.25, 0, 0, mat); + m4.zRotate(mat, time * (0.1 + 0.1 * ndx), mat); + + const color = colors[ndx]; + + gl.uniform4fv(colorLoc, color); + gl.uniformMatrix4fv(matrixLoc, false, mat); + + gl.drawArrays( + gl.TRIANGLES, + 0, // offset + numVertices, // количество вершин на экземпляр + ); + }); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +Обратите внимание, что библиотека матриц принимает необязательную матрицу-назначение +в конце каждой функции. В большинстве статей мы не использовали эту возможность и просто +давали библиотеке создавать новую матрицу, но здесь мы хотим, чтобы результат +сохранялся в уже созданных матрицах. + +Это работает, и мы получаем 5 вращающихся плюсов разного цвета. + +{{{example url="../webgl-instanced-drawing-not-instanced.html"}}} + +Это потребовало 5 вызовов `gl.uniform4v`, 5 вызовов `gl.uniformMatrix4fv` +и 5 вызовов `gl.drawArrays`, всего 15 вызовов WebGL. Если бы наши шейдеры были сложнее, +например, как в [статье про spot lighting](webgl-3d-lighting-spot.html), +было бы минимум 7 вызовов на объект: 6 к `gl.uniformXXX` и один к `gl.drawArrays`. +Если бы мы рисовали 400 объектов, это было бы 2800 вызовов WebGL. + +Инстансинг позволяет уменьшить количество вызовов. Он работает так: +вы указываете WebGL, сколько раз нужно нарисовать один и тот же объект (количество экземпляров). +Для каждого атрибута вы указываете, будет ли он переходить к *следующему значению* из буфера +каждый раз при вызове вершинного шейдера (по умолчанию), или только раз в N экземпляров (обычно N=1). + +Например, вместо передачи `matrix` и `color` через uniform, мы передадим их через атрибуты. +Положим матрицы и цвета для каждого экземпляра в буфер, настроим атрибуты для чтения из этих буферов +и скажем WebGL, чтобы он переходил к следующему значению только раз на экземпляр. + +Давайте сделаем это! + +Сначала изменим шейдеры, чтобы использовать атрибуты для `matrix` и `color` вместо uniform'ов. + +```js +const vertexShaderSource = `#version 300 es +in vec4 a_position; +uniform mat4 matrix; + +void main() { + // Умножаем позицию на матрицу + gl_Position = matrix * a_position; +} +`; + +const fragmentShaderSource = `#version 300 es +precision highp float; + +uniform vec4 color; + +out vec4 outColor; + +void main() { + outColor = color; +} +`; +``` + +--- + +Теперь нам нужно реализовать инстансинг через атрибуты и буферы: + +```js +// настраиваем матрицы, по одной на экземпляр +const numInstances = 5; +// создаём типизированный массив с одним view на матрицу +const matrixData = new Float32Array(numInstances * 16); +const matrices = []; +for (let i = 0; i < numInstances; ++i) { + const byteOffsetToMatrix = i * 16 * 4; + const numFloatsForView = 16; + matrices.push(new Float32Array( + matrixData.buffer, + byteOffsetToMatrix, + numFloatsForView)); +} + +const matrixBuffer = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); +gl.bufferData(gl.ARRAY_BUFFER, matrixData.byteLength, gl.DYNAMIC_DRAW); + +const bytesPerMatrix = 4 * 16; +for (let i = 0; i < 4; ++i) { + const loc = matrixLoc + i; + gl.enableVertexAttribArray(loc); + // stride и offset + const offset = i * 16; + gl.vertexAttribPointer( + loc, + 4, + gl.FLOAT, + false, + bytesPerMatrix, + offset, + ); + gl.vertexAttribDivisor(loc, 1); +} + +// настраиваем цвета, по одному на экземпляр +const colorBuffer = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); +gl.bufferData(gl.ARRAY_BUFFER, + new Float32Array([ + 1, 0, 0, 1, // красный + 0, 1, 0, 1, // зелёный + 0, 0, 1, 1, // синий + 1, 0, 1, 1, // маджента + 0, 1, 1, 1, // циан + ]), + gl.STATIC_DRAW); +gl.enableVertexAttribArray(colorLoc); +gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, 0, 0); +gl.vertexAttribDivisor(colorLoc, 1); + +// В рендере: +function render(time) { + time *= 0.001; + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.useProgram(program); + gl.bindVertexArray(vao); + + // обновляем все матрицы + matrices.forEach((mat, ndx) => { + m4.translation(-0.5 + ndx * 0.25, 0, 0, mat); + m4.zRotate(mat, time * (0.1 + 0.1 * ndx), mat); + }); + // загружаем все матрицы в буфер + gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, matrixData); + + // рисуем все экземпляры одной командой + gl.drawArraysInstanced( + gl.TRIANGLES, + 0, // offset + numVertices, // количество вершин на экземпляр + numInstances // количество экземпляров + ); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +Теперь мы вызываем только одну команду отрисовки, и WebGL сам перебирает экземпляры, используя данные из буферов для каждого экземпляра. + +{{{example url="../webgl-instanced-drawing.html"}}} + +Инстансинг — мощный способ ускорить отрисовку множества одинаковых объектов с разными параметрами. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-less-code-more-fun.md b/webgl/lessons/ru/webgl-less-code-more-fun.md new file mode 100644 index 000000000..c3ba16672 --- /dev/null +++ b/webgl/lessons/ru/webgl-less-code-more-fun.md @@ -0,0 +1,199 @@ +Title: WebGL2 — Меньше кода, больше удовольствия +Description: Как сделать программирование на WebGL менее многословным +TOC: Меньше кода, больше удовольствия + + +Этот пост — продолжение серии статей о WebGL. +Первая [начиналась с основ](webgl-fundamentals.html). +Если вы их не читали, начните с них. + +В WebGL-программах нужно писать шейдеры, компилировать и линковать их, а затем +искать локации входов этих шейдеров. Эти входы называются +uniform'ами и атрибутами, и код для поиска их локаций может быть многословным и утомительным. + +Допустим, у нас есть типовой boilerplate-код для компиляции и линковки шейдеров. +Пусть у нас такие шейдеры: + +Вершинный шейдер: + +``` +#version 300 es + +uniform mat4 u_worldViewProjection; +uniform vec3 u_lightWorldPos; +uniform mat4 u_world; +uniform mat4 u_viewInverse; +uniform mat4 u_worldInverseTranspose; + +in vec4 a_position; +in vec3 a_normal; +in vec2 a_texcoord; + +out vec4 v_position; +out vec2 v_texCoord; +out vec3 v_normal; +out vec3 v_surfaceToLight; +out vec3 v_surfaceToView; + +void main() { + v_texCoord = a_texcoord; + v_position = (u_worldViewProjection * a_position); + v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz; + v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz; + v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz; + gl_Position = v_position; +} +``` + +Фрагментный шейдер: + +``` +#version 300 es +precision highp float; + +in vec4 v_position; +in vec2 v_texCoord; +in vec3 v_normal; +in vec3 v_surfaceToLight; +in vec3 v_surfaceToView; + +uniform vec4 u_lightColor; +uniform vec4 u_ambient; +uniform sampler2D u_diffuse; +uniform vec4 u_specular; +uniform float u_shininess; +uniform float u_specularFactor; + +out vec4 outColor; + +vec4 lit(float l ,float h, float m) { + return vec4(1.0, + max(l, 0.0), + (l > 0.0) ? pow(max(0.0, h), m) : 0.0, + 1.0); +} + +void main() { + vec4 diffuseColor = texture(u_diffuse, v_texCoord); + vec3 a_normal = normalize(v_normal); + vec3 surfaceToLight = normalize(v_surfaceToLight); + vec3 surfaceToView = normalize(v_surfaceToView); + vec3 halfVector = normalize(surfaceToLight + surfaceToView); + vec4 litR = lit(dot(a_normal, surfaceToLight), + dot(a_normal, halfVector), u_shininess); + outColor = vec4(( + u_lightColor * (diffuseColor * litR.y + diffuseColor * u_ambient + + u_specular * litR.z * u_specularFactor)).rgb, + diffuseColor.a); +} +``` + +Вам пришлось бы писать такой код для поиска и установки всех значений для отрисовки: + +``` +// При инициализации +var u_worldViewProjectionLoc = gl.getUniformLocation(program, "u_worldViewProjection"); +var u_lightWorldPosLoc = gl.getUniformLocation(program, "u_lightWorldPos"); +var u_worldLoc = gl.getUniformLocation(program, "u_world"); +var u_viewInverseLoc = gl.getUniformLocation(program, "u_viewInverse"); +var u_worldInverseTransposeLoc = gl.getUniformLocation(program, "u_worldInverseTranspose"); +var u_lightColorLoc = gl.getUniformLocation(program, "u_lightColor"); +var u_ambientLoc = gl.getUniformLocation(program, "u_ambient"); +var u_diffuseLoc = gl.getUniformLocation(program, "u_diffuse"); +var u_specularLoc = gl.getUniformLocation(program, "u_specular"); +var u_shininessLoc = gl.getUniformLocation(program, "u_shininess"); +var u_specularFactorLoc = gl.getUniformLocation(program, "u_specularFactor"); + +var a_positionLoc = gl.getAttribLocation(program, "a_position"); +var a_normalLoc = gl.getAttribLocation(program, "a_normal"); +var a_texCoordLoc = gl.getAttribLocation(program, "a_texcoord"); + +// Настраиваем все буферы и атрибуты (предполагаем, что буферы уже созданы) +var vao = gl.createVertexArray(); +gl.bindVertexArray(vao); +gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); +gl.enableVertexAttribArray(a_positionLoc); +gl.vertexAttribPointer(a_positionLoc, positionNumComponents, gl.FLOAT, false, 0, 0); +gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); +gl.enableVertexAttribArray(a_normalLoc); +gl.vertexAttribPointer(a_normalLoc, normalNumComponents, gl.FLOAT, false, 0, 0); +gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); +gl.enableVertexAttribArray(a_texcoordLoc); +gl.vertexAttribPointer(a_texcoordLoc, texcoordNumComponents, gl.FLOAT, false, 0, 0); + +// При инициализации или отрисовке, в зависимости от задачи +var someWorldViewProjectionMat = computeWorldViewProjectionMatrix(); +var lightWorldPos = [100, 200, 300]; +var worldMat = computeWorldMatrix(); +var viewInverseMat = computeInverseViewMatrix(); +var worldInverseTransposeMat = computeWorldInverseTransposeMatrix(); +var lightColor = [1, 1, 1, 1]; +var ambientColor = [0.1, 0.1, 0.1, 1]; +var diffuseTextureUnit = 0; +var specularColor = [1, 1, 1, 1]; +var shininess = 60; +var specularFactor = 1; + +// При отрисовке +gl.useProgram(program); +gl.bindVertexArray(vao); + +gl.activeTexture(gl.TEXTURE0 + diffuseTextureUnit); +gl.bindTexture(gl.TEXTURE_2D, diffuseTexture); + +gl.uniformMatrix4fv(u_worldViewProjectionLoc, false, someWorldViewProjectionMat); +gl.uniform3fv(u_lightWorldPosLoc, lightWorldPos); +gl.uniformMatrix4fv(u_worldLoc, worldMat); +gl.uniformMatrix4fv(u_viewInverseLoc, viewInverseMat); +gl.uniformMatrix4fv(u_worldInverseTransposeLoc, worldInverseTransposeMat); +gl.uniform4fv(u_lightColorLoc, lightColor); +gl.uniform4fv(u_ambientLoc, ambientColor); +gl.uniform1i(u_diffuseLoc, diffuseTextureUnit); +gl.uniform4fv(u_specularLoc, specularColor); +gl.uniform1f(u_shininessLoc, shininess); +gl.uniform1f(u_specularFactorLoc, specularFactor); + +gl.drawArrays(...); +``` + +Это очень много кода. + +Есть много способов упростить это. Один из вариантов — попросить WebGL выдать все +uniform'ы, атрибуты и их локации, а затем создать функции для их установки. +Тогда можно будет передавать обычные JavaScript-объекты для настройки параметров. +Если это звучит непонятно, вот как выглядел бы код: + +``` +// При инициализации +var uniformSetters = twgl.createUniformSetters(gl, program); +var attribSetters = twgl.createAttributeSetters(gl, program); + +// Настраиваем все буферы и атрибуты +var attribs = { + a_position: { buffer: positionBuffer, numComponents: 3, }, + a_normal: { buffer: normalBuffer, numComponents: 3, }, + a_texcoord: { buffer: texcoordBuffer, numComponents: 2, }, +}; +var vao = twgl.createVAOAndSetAttributes( + gl, attribSetters, attribs); + +// При инициализации или отрисовке +var uniforms = { + u_worldViewProjection: computeWorldViewProjectionMatrix(...), + u_lightWorldPos: [100, 200, 300], + u_world: computeWorldMatrix(), + u_viewInverse: computeInverseViewMatrix(), + u_worldInverseTranspose: computeWorldInverseTransposeMatrix(), + u_lightColor: [1, 1, 1, 1], + u_ambient: [0.1, 0.1, 0.1, 1], + u_diffuse: diffuseTexture, + u_specular: [1, 1, 1, 1], + u_shininess: 60, + u_specularFactor: 1, +}; + +// При отрисовке +gl.useProgram(program); + +// Привязываем VAO, в котором уже все буферы и атрибуты +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-load-obj-w-mtl.md b/webgl/lessons/ru/webgl-load-obj-w-mtl.md new file mode 100644 index 000000000..f3f66f014 --- /dev/null +++ b/webgl/lessons/ru/webgl-load-obj-w-mtl.md @@ -0,0 +1,500 @@ +Title: WebGL2 Загрузка Obj с Mtl +Description: Как парсить .MTL файл +TOC: Загрузка .obj с .mtl файлами + +В [предыдущей статье](webgl-load-obj.html) мы разбирали парсинг .OBJ файлов. +В этой статье разберём их дополнительные .MTL (material) файлы. + +**Дисклеймер:** Этот парсер .MTL не претендует на полноту или +идеальность и не обрабатывает каждый возможный .MTL-файл. Его цель — +показать подход к обработке того, что встречается на практике. +Если вы столкнётесь с серьёзными проблемами и решениями — оставьте комментарий, +это может помочь другим. + +Мы загрузили этот [CC-BY 4.0](http://creativecommons.org/licenses/by/4.0/) [стул](https://sketchfab.com/3d-models/chair-aa2acddb218646a59ece132bf95aa558) от [haytonm](https://sketchfab.com/haytonm) с [Sketchfab](https://sketchfab.com/) + +
+ +У него есть соответствующий .MTL файл, который выглядит так: + +``` +# Blender MTL File: 'None' +# Material Count: 11 + +newmtl D1blinn1SG +Ns 323.999994 +Ka 1.000000 1.000000 1.000000 +Kd 0.500000 0.500000 0.500000 +Ks 0.500000 0.500000 0.500000 +Ke 0.0 0.0 0.0 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl D1lambert2SG +Ns 323.999994 +Ka 1.000000 1.000000 1.000000 +Kd 0.020000 0.020000 0.020000 +Ks 0.500000 0.500000 0.500000 +Ke 0.0 0.0 0.0 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl D1lambert3SG +Ns 323.999994 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.0 0.0 0.0 +Ni 1.000000 +d 1.000000 +illum 2 + +... аналогично для ещё 8 материалов +``` + +Если посмотреть [описание формата .MTL](http://paulbourke.net/dataformats/mtl/), +то видно, что ключевое слово `newmtl` начинает новый материал с заданным именем, а ниже +идут все параметры этого материала. Каждая строка начинается с ключевого слова, как и в .OBJ, +поэтому можно начать с похожего каркаса: + +```js +function parseMTL(text) { + const materials = {}; + let material; + + const keywords = { + newmtl(parts, unparsedArgs) { + material = {}; + materials[unparsedArgs] = material; + }, + }; + + const keywordRE = /(cw*)(?: )*(.*)/; + const lines = text.split('\n'); + for (let lineNo = 0; lineNo < lines.length; ++lineNo) { + const line = lines[lineNo].trim(); + if (line === '' || line.startsWith('#')) { + continue; + } + const m = keywordRE.exec(line); + if (!m) { + continue; + } + const [, keyword, unparsedArgs] = m; + const parts = line.split(/\s+/).slice(1); + const handler = keywords[keyword]; + if (!handler) { + console.warn('unhandled keyword:', keyword); + continue; + } + handler(parts, unparsedArgs); + } + + return materials; +} +``` + +Далее нужно добавить обработчики для каждого ключевого слова. Документация говорит: + +* `Ns` — specular shininess (см. [статью про точечный свет](webgl-3d-lighting-point.html)) +* `Ka` — ambient-цвет материала +* `Kd` — diffuse-цвет (наш основной цвет в [статье про точечный свет](webgl-3d-lighting-point.html)) +* `Ks` — specular-цвет +* `Ke` — emissive-цвет +* `Ni` — оптическая плотность (не используем) +* `d` — "dissolve", прозрачность +* `illum` — тип освещения (всего 11 видов, пока игнорируем) + +Я думал, оставить ли эти имена как есть. Математикам нравятся короткие имена, но +в большинстве стайлгайдов предпочитают описательные. Я выбрал второй вариант: + +```js +function parseMTL(text) { + const materials = {}; + let material; + + const keywords = { + newmtl(parts, unparsedArgs) { + material = {}; + materials[unparsedArgs] = material; + }, + Ns(parts) { material.shininess = parseFloat(parts[0]); }, + Ka(parts) { material.ambient = parts.map(parseFloat); }, + Kd(parts) { material.diffuse = parts.map(parseFloat); }, + Ks(parts) { material.specular = parts.map(parseFloat); }, + Ke(parts) { material.emissive = parts.map(parseFloat); }, + Ni(parts) { material.opticalDensity = parseFloat(parts[0]); }, + d(parts) { material.opacity = parseFloat(parts[0]); }, + illum(parts) { material.illum = parseInt(parts[0]); }, + }; + + ... + + return materials; +} +``` + +Два материала могут ссылаться на одну и ту же текстуру, поэтому будем хранить все текстуры в объекте по имени файла, чтобы не загружать одну и ту же текстуру несколько раз. + +```js +const textures = { + defaultWhite: twgl.createTexture(gl, {src: [255, 255, 255, 255]}), +}; + +// загружаем текстуры для материалов +for (const material of Object.values(materials)) { + Object.entries(material) + .filter(([key]) => key.endsWith('Map')) + .forEach(([key, filename]) => { + let texture = textures[filename]; + if (!texture) { + const textureHref = new URL(filename, baseHref).href; + texture = twgl.createTexture(gl, {src: textureHref, flipY: true}); + textures[filename] = texture; + } + material[key] = texture; + }); +} +``` + +Этот код проходит по каждому свойству каждого материала. Если имя свойства заканчивается на "Map", создаётся относительный URL, создаётся текстура и присваивается обратно в материал. Хелпер асинхронно загрузит изображение в текстуру. + +Также добавим текстуру-«заглушку» — белый пиксель, которую можно использовать для любого материала без текстуры. Так мы сможем использовать один и тот же шейдер для всех материалов. Иначе пришлось бы делать разные шейдеры для материалов с текстурой и без. + +```js +const defaultMaterial = { + diffuse: [1, 1, 1], + diffuseMap: textures.defaultWhite, + ambient: [0, 0, 0], + specular: [1, 1, 1], + shininess: 400, + opacity: 1, +}; + +const parts = obj.geometries.map(({material, data}) => { + + ... + + // создаём буфер для каждого массива + const bufferInfo = twgl.createBufferInfoFromArrays(gl, data); + const vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, bufferInfo); + return { + material: { + ...defaultMaterial, + ...materials[material], + }, + bufferInfo, + vao, + }; +}); +``` + +Чтобы использовать текстуры, нужно изменить шейдер. Начнём с diffuse map. + +```js +const vs = `#version 300 es +in vec4 a_position; +in vec3 a_normal; +in vec2 a_texcoord; +in vec4 a_color; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; +uniform vec3 u_viewWorldPosition; + +out vec3 v_normal; +out vec3 v_surfaceToView; +out vec2 v_texcoord; +out vec4 v_color; + +void main() { + vec4 worldPosition = u_world * a_position; + gl_Position = u_projection * u_view * worldPosition; + v_surfaceToView = u_viewWorldPosition - worldPosition.xyz; + v_normal = mat3(u_world) * a_normal; + v_texcoord = a_texcoord; + v_color = a_color; +} +`; + +const fs = `#version 300 es +precision highp float; + +in vec3 v_normal; +in vec3 v_surfaceToView; +in vec2 v_texcoord; +in vec4 v_color; + +uniform vec3 diffuse; +uniform sampler2D diffuseMap; +uniform vec3 ambient; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; +uniform vec3 u_lightDirection; +uniform vec3 u_ambientLight; + +out vec4 outColor; + +void main () { + vec3 normal = normalize(v_normal); + + vec3 surfaceToViewDirection = normalize(v_surfaceToView); + vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection); + + float fakeLight = dot(u_lightDirection, normal) * .5 + .5; + float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0); + vec4 specularMapColor = texture(specularMap, v_texcoord); + vec3 effectiveSpecular = specular * specularMapColor.rgb; + + vec4 diffuseMapColor = texture(diffuseMap, v_texcoord); + vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb; + float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a; + + outColor = vec4( + emissive + + ambient * u_ambientLight + + effectiveDiffuse * fakeLight + + effectiveSpecular * pow(specularLight, shininess), + effectiveOpacity); +} +`; +``` + +Теперь у нас есть текстуры! + +{{{example url="../webgl-load-obj-w-mtl-w-textures.html"}}} + +Если посмотреть на .MTL-файл, можно увидеть `map_Ks` — это чёрно-белая текстура, которая определяет, насколько поверхность блестящая, или, иначе говоря, сколько specular-отражения используется. + +
+ +Чтобы использовать её, нужно обновить шейдер, ведь мы уже загружаем все текстуры. + +```js +const fs = `#version 300 es +precision highp float; + +in vec3 v_normal; +in vec3 v_surfaceToView; +in vec2 v_texcoord; +in vec4 v_color; + +uniform vec3 diffuse; +uniform sampler2D diffuseMap; +uniform vec3 ambient; +uniform vec3 emissive; +uniform vec3 specular; +uniform sampler2D specularMap; +uniform float shininess; +uniform float opacity; +uniform vec3 u_lightDirection; +uniform vec3 u_ambientLight; + +out vec4 outColor; + +void main () { + vec3 normal = normalize(v_normal); + + vec3 surfaceToViewDirection = normalize(v_surfaceToView); + vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection); + + float fakeLight = dot(u_lightDirection, normal) * .5 + .5; + float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0); + vec4 specularMapColor = texture(specularMap, v_texcoord); + vec3 effectiveSpecular = specular * specularMapColor.rgb; + + vec4 diffuseMapColor = texture(diffuseMap, v_texcoord); + vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb; + float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a; + + outColor = vec4( + emissive + + ambient * u_ambientLight + + effectiveDiffuse * fakeLight + + effectiveSpecular * pow(specularLight, shininess), + effectiveOpacity); +``` + +Также стоит добавить значение по умолчанию для материалов без карты specular: + +```js +const defaultMaterial = { + diffuse: [1, 1, 1], + diffuseMap: textures.defaultWhite, + ambient: [0, 0, 0], + specular: [1, 1, 1], + specularMap: textures.defaultWhite, + shininess: 400, + opacity: 1, +}; +``` + +В .MTL-файле значения specular могут быть не очень наглядными, поэтому для наглядности можно «взломать» параметры specular: + +```js +// хак: делаем specular заметнее +Object.values(materials).forEach(m => { + m.shininess = 25; + m.specular = [3, 2, 1]; +}); +``` + +Теперь видно, что только окна и лопасти отражают свет. + +{{{example url="../webgl-load-obj-w-mtl-w-specular-map.html"}}} + +Меня удивило, что лопасти отражают свет. Если посмотреть на .MTL-файл, там shininess `Ns` = 0.0, что означает очень сильные specular-блики. Но illum = 1 для обоих материалов. По документации illum 1 означает: + +``` +color = KaIa + Kd { SUM j=1..ls, (N * Lj)Ij } +``` + +То есть: + +``` +color = ambientColor * lightAmbient + diffuseColor * sumOfLightCalculations +``` + +Как видно, specular тут не участвует, но в файле всё равно есть specular map! ¯\_(ツ)_/¯ Для specular-бликов нужен illum 2 или выше. Это типичная ситуация с .OBJ/.MTL: часто приходится вручную дорабатывать материалы. Как исправлять — решать вам: можно править .MTL, можно добавить код. Мы выбрали второй путь. + +Последняя карта в этом .MTL — `map_Bump` (bump map). На самом деле файл — это normal map. + +
+ +В .MTL нет опции явно указать normal map или что bump map — это normal map. Можно использовать эвристику: если в имени файла есть 'nor', или просто считать, что все `map_Bump` — это normal map (по крайней мере в 2020+). Так и поступим. + +Для генерации тангенсов используем код из [статьи про normal mapping](webgl-3d-lighting-normal-mapping.html): + +```js +const parts = obj.geometries.map(({material, data}) => { + ... + + // генерируем тангенсы, если есть данные + if (data.texcoord && data.normal) { + data.tangent = generateTangents(data.position, data.texcoord); + } else { + // Нет тангенсов + data.tangent = { value: [1, 0, 0] }; + } + + // создаём буфер для каждого массива + const bufferInfo = twgl.createBufferInfoFromArrays(gl, data); + const vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, bufferInfo); + return { + material: { + ...defaultMaterial, + ...materials[material], + }, + bufferInfo, + vao, + }; +}); +``` + +Также добавим normal map по умолчанию для материалов, у которых его нет: + +```js +const textures = { + defaultWhite: twgl.createTexture(gl, {src: [255, 255, 255, 255]}), + defaultNormal: twgl.createTexture(gl, {src: [127, 127, 255, 0]}), +}; + +... + +const defaultMaterial = { + diffuse: [1, 1, 1], + diffuseMap: textures.defaultWhite, + normalMap: textures.defaultNormal, + ambient: [0, 0, 0], + specular: [1, 1, 1], + specularMap: textures.defaultWhite, + shininess: 400, + opacity: 1, +}; +... +``` + +И, наконец, вносим изменения в шейдеры, как в [статье про normal mapping](webgl-3d-lighting-normal-mapping.html): + +```js +const vs = `#version 300 es +in vec4 a_position; +in vec3 a_normal; +in vec3 a_tangent; +in vec2 a_texcoord; +in vec4 a_color; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; +uniform vec3 u_viewWorldPosition; + +out vec3 v_normal; +out vec3 v_tangent; +out vec3 v_surfaceToView; +out vec2 v_texcoord; +out vec4 v_color; + +void main() { + vec4 worldPosition = u_world * a_position; + gl_Position = u_projection * u_view * worldPosition; + v_surfaceToView = u_viewWorldPosition - worldPosition.xyz; + + mat3 normalMat = mat3(u_world); + v_normal = normalize(normalMat * a_normal); + v_tangent = normalize(normalMat * a_tangent); + + v_texcoord = a_texcoord; + v_color = a_color; +} +`; + +const fs = `#version 300 es +precision highp float; + +in vec3 v_normal; +in vec3 v_tangent; +in vec3 v_surfaceToView; +in vec2 v_texcoord; +in vec4 v_color; + +uniform vec3 diffuse; +uniform sampler2D diffuseMap; +uniform vec3 ambient; +uniform vec3 emissive; +uniform vec3 specular; +uniform sampler2D specularMap; +uniform float shininess; +uniform sampler2D normalMap; +uniform float opacity; +uniform vec3 u_lightDirection; +uniform vec3 u_ambientLight; + +out vec4 outColor; + +void main () { + vec3 normal = normalize(v_normal); + vec3 tangent = normalize(v_tangent); + vec3 bitangent = normalize(cross(normal, tangent)); + + mat3 tbn = mat3(tangent, bitangent, normal); + normal = texture(normalMap, v_texcoord).rgb * 2. - 1.; + normal = normalize(tbn * normal); + + vec3 surfaceToViewDirection = normalize(v_surfaceToView); + vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection); + + float fakeLight = dot(u_lightDirection, normal) * .5 + .5; + float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0); + vec4 specularMapColor = texture(specularMap, v_texcoord); + vec3 effectiveSpecular = specular * specularMapColor.rgb; + + vec4 diffuseMapColor = texture(diffuseMap, v_texcoord); + vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb; + float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a; +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-load-obj.md b/webgl/lessons/ru/webgl-load-obj.md new file mode 100644 index 000000000..54947608c --- /dev/null +++ b/webgl/lessons/ru/webgl-load-obj.md @@ -0,0 +1,572 @@ +Title: WebGL2 Загрузка Obj +Description: Как парсить и отображать .OBJ файл +TOC: Загрузка .obj файлов + +Файлы Wavefront .obj являются одним из самых распространенных форматов +3D файлов, которые вы можете найти в интернете. Они не так сложны для +парсинга в самых распространенных формах, поэтому давайте разберем один. Это +надеюсь, предоставит полезный пример для парсинга 3D форматов +в общем. + +**Отказ от ответственности:** Этот парсер .OBJ не предназначен для того, чтобы быть исчерпывающим или +безупречным или обрабатывать каждый .OBJ файл. Скорее он предназначен как +упражнение для прохождения через обработку того, с чем мы сталкиваемся по пути. +Тем не менее, если вы столкнетесь с большими проблемами и решениями, комментарий +внизу может быть полезен для других, если они решат +использовать этот код. + +Лучшая документация, которую я нашел для формата .OBJ, находится +[здесь](http://paulbourke.net/dataformats/obj/). Хотя +[эта страница](https://www.loc.gov/preservation/digital/formats/fdd/fdd000507.shtml) +ссылается на многие другие документы, включая то, что кажется +[оригинальной документацией](https://web.archive.org/web/20200324065233/http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf). + +Давайте посмотрим на простой пример. Вот файл cube.obj, экспортированный из стандартной сцены blender. + +```txt +# Blender v2.80 (sub 75) OBJ File: '' +# www.blender.org +mtllib cube.mtl +o Cube +v 1.000000 1.000000 -1.000000 +v 1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +v -1.000000 -1.000000 -1.000000 +v -1.000000 1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +vt 0.375000 0.000000 +vt 0.625000 0.000000 +vt 0.625000 0.250000 +vt 0.375000 0.250000 +vt 0.375000 0.250000 +vt 0.625000 0.250000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.625000 0.750000 +vt 0.625000 1.000000 +vt 0.375000 1.000000 +vt 0.125000 0.500000 +vt 0.375000 0.500000 +vt 0.375000 0.750000 +vt 0.125000 0.750000 +vt 0.625000 0.500000 +vt 0.875000 0.500000 +vt 0.875000 0.750000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 0.0000 1.0000 +vn -1.0000 0.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn 1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +usemtl Material +s off +f 1/1/1 5/2/1 7/3/1 3/4/1 +f 4/5/2 3/6/2 7/7/2 8/8/2 +f 8/8/3 7/7/3 5/9/3 6/10/3 +f 6/10/4 2/11/4 4/12/4 8/13/4 +f 2/14/5 1/15/5 3/16/5 4/17/5 +f 6/18/6 5/19/6 1/20/6 2/11/6 +``` + +Даже не глядя на документацию, мы, вероятно, можем понять, +что строки, начинающиеся с `v`, являются позициями, строки, начинающиеся +с `vt`, являются координатами текстуры, и строки, начинающиеся +с `vn`, являются нормалями. Осталось разобраться с остальным. + +Похоже, что .OBJ файлы являются текстовыми файлами, поэтому первое, что нам нужно +сделать, это загрузить текстовый файл. К счастью, в 2020 году это очень просто, +если мы используем [async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await). + +```js +async function main() { + ... + + const response = await fetch('resources/models/cube/cube.obj'); + const text = await response.text(); +``` + +Далее выглядит так, что мы можем парсить его построчно, и что +каждая строка имеет форму + +``` +ключевое_слово данные данные данные данные ... +``` + +где первая вещь в строке - это ключевое слово, а данные +разделены пробелами. Строки, начинающиеся с `#`, являются комментариями. + +Итак, давайте настроим код для парсинга каждой строки, пропуска пустых строк +и комментариев, а затем вызова некоторой функции на основе ключевого слова + +```js ++function parseOBJ(text) { ++ ++ const keywords = { ++ }; ++ ++ const keywordRE = /(\w*)(?: )*(.*)/; ++ const lines = text.split('\n'); ++ for (let lineNo = 0; lineNo < lines.length; ++lineNo) { ++ const line = lines[lineNo].trim(); ++ if (line === '' || line.startsWith('#')) { ++ continue; ++ } ++ const parts = line.split(/\s+/); ++ const m = keywordRE.exec(line); ++ if (!m) { ++ continue; ++ } ++ const [, keyword, unparsedArgs] = m; ++ const parts = line.split(/\s+/).slice(1); ++ const handler = keywords[keyword]; ++ if (!handler) { ++ console.warn('unhandled keyword:', keyword, 'at line', lineNo + 1); ++ continue; ++ } ++ handler(parts, unparsedArgs); ++ } +} +``` + +Некоторые вещи для заметки: Мы обрезаем каждую строку, чтобы удалить ведущие и завершающие +пробелы. Я не знаю, нужно ли это, но думаю, что это не может навредить. +Мы разделяем строку по пробелам, используя `/\s+/`. Снова я не знаю, нужно ли это. +Может ли быть больше одного пробела между данными? Могут ли +быть табуляции? Не знаю, но казалось безопаснее предположить, что есть вариации +там, учитывая, что это текстовый формат. + +В противном случае мы извлекаем первую часть как ключевое слово, а затем ищем функцию +для этого ключевого слова и вызываем ее, передавая данные после ключевого слова. Итак, теперь нам +просто нужно заполнить эти функции. + +Мы угадали данные `v`, `vt` и `vn` выше. Документация говорит, что `f` +означает "face" или полигон, где каждый кусок данных является +индексами в позиции, координаты текстуры и нормали. + +Индексы основаны на 1, если положительные, или относительны к количеству +вершин, разобранных до сих пор, если отрицательные. +Порядок индексов: позиция/текстурная_координата/нормаль, и +что все, кроме позиции, являются необязательными, поэтому + +```txt +f 1 2 3 # индексы только для позиций +f 1/1 2/2 3/3 # индексы для позиций и текстурных координат +f 1/1/1 2/2/2 3/3/3 # индексы для позиций, текстурных координат и нормалей +f 1//1 2//2 3//3 # индексы для позиций и нормалей +``` + +`f` может иметь больше 3 вершин, например 4 для четырехугольника +Мы знаем, что WebGL может рисовать только треугольники, поэтому нам нужно конвертировать +данные в треугольники. Не сказано, может ли грань иметь больше +4 вершин, ни не сказано, должна ли грань быть выпуклой или +может ли она быть вогнутой. Пока давайте предположим, что они вогнутые. + +Также, в общем, в WebGL мы не используем разные индексы для +позиций, текстурных координат и нормалей. Вместо этого "webgl вершина" +является комбинацией всех данных для этой вершины. Так, например, +чтобы нарисовать куб, WebGL требует 36 вершин, каждая грань - это 2 треугольника, +каждый треугольник - это 3 вершины. 6 граней * 2 треугольника * 3 вершины +на треугольник = 36. Хотя есть только 8 уникальных позиций, +6 уникальных нормалей и кто знает для текстурных координат. Итак, нам +нужно будет прочитать индексы вершин грани и сгенерировать "webgl вершину", +которая является комбинацией данных всех 3 вещей. [*](webgl-pulling-vertices.html) + +Итак, учитывая все это, выглядит так, что мы можем парсить эти части следующим образом + +```js +function parseOBJ(text) { ++ // потому что индексы основаны на 1, давайте просто заполним 0-е данные ++ const objPositions = [[0, 0, 0]]; ++ const objTexcoords = [[0, 0]]; ++ const objNormals = [[0, 0, 0]]; ++ ++ // тот же порядок, что и индексы `f` ++ const objVertexData = [ ++ objPositions, ++ objTexcoords, ++ objNormals, ++ ]; ++ ++ // тот же порядок, что и индексы `f` ++ let webglVertexData = [ ++ [], // позиции ++ [], // текстурные координаты ++ [], // нормали ++ ]; ++ ++ function addVertex(vert) { ++ const ptn = vert.split('/'); ++ ptn.forEach((objIndexStr, i) => { ++ if (!objIndexStr) { ++ return; ++ } ++ const objIndex = parseInt(objIndexStr); ++ const index = objIndex + (objIndex >= 0 ? 0 : objVertexData[i].length); ++ webglVertexData[i].push(...objVertexData[i][index]); ++ }); ++ } ++ ++ const keywords = { ++ v(parts) { ++ objPositions.push(parts.map(parseFloat)); ++ }, ++ vn(parts) { ++ objNormals.push(parts.map(parseFloat)); ++ }, ++ vt(parts) { ++ objTexcoords.push(parts.map(parseFloat)); ++ }, ++ f(parts) { ++ const numTriangles = parts.length - 2; ++ for (let tri = 0; tri < numTriangles; ++tri) { ++ addVertex(parts[0]); ++ addVertex(parts[tri + 1]); ++ addVertex(parts[tri + 2]); ++ } ++ }, ++ }; ++ ++ const keywordRE = /(\w*)(?: )*(.*)/; ++ const lines = text.split('\n'); ++ for (let lineNo = 0; lineNo < lines.length; ++lineNo) { ++ const line = lines[lineNo].trim(); ++ if (line === '' || line.startsWith('#')) { ++ continue; ++ } ++ const m = keywordRE.exec(line); ++ if (!m) { ++ continue; ++ } ++ const [, keyword, unparsedArgs] = m; ++ const parts = line.split(/\s+/).slice(1); ++ const handler = keywords[keyword]; ++ if (!handler) { ++ console.warn('unhandled keyword:', keyword, 'at line', lineNo + 1); ++ continue; ++ } ++ handler(parts, unparsedArgs); ++ } + + return { + position: webglVertexData[0], + texcoord: webglVertexData[1], + normal: webglVertexData[2], + }; +} + +Код выше создает 3 массива для хранения позиций, текстурных координат и +нормалей, разобранных из файла объекта. Он также создает 3 массива для хранения +того же для WebGL. Они также помещены в массивы в том же порядке, +что и индексы `f`, чтобы было легко ссылаться при парсинге `f`. + +Другими словами, строка `f` типа + +```txt +f 1/2/3 4/5/6 7/8/9 +``` + +Одна из этих частей `4/5/6` говорит "используй позицию 4" для этой вершины грани, "используй +текстурную координату 5" и "используй нормаль 6", но помещая массивы позиций, текстурных координат и нормалей +самих в массив, массив `objVertexData`, мы можем упростить это до +"используй элемент n из objData i для webglData i", что позволяет нам сделать код проще. + +В конце нашей функции мы возвращаем данные, которые мы построили + +```js + ... + + return { + position: webglVertexData[0], + texcoord: webglVertexData[1], + normal: webglVertexData[2], + }; +} +``` + +Все, что осталось сделать, это нарисовать данные. Сначала мы будем использовать вариацию +шейдеров из [статьи о направленном освещении](webgl-3d-lighting-directional.html). + +```js +const vs = `#version 300 es + in vec4 a_position; + in vec3 a_normal; + + uniform mat4 u_projection; + uniform mat4 u_view; + uniform mat4 u_world; + + out vec3 v_normal; + + void main() { + gl_Position = u_projection * u_view * u_world * a_position; + v_normal = mat3(u_world) * a_normal; + } +`; + +const fs = `#version 300 es + precision highp float; + + in vec3 v_normal; + + uniform vec4 u_diffuse; + uniform vec3 u_lightDirection; + + out vec4 outColor; + + void main () { + vec3 normal = normalize(v_normal); + float fakeLight = dot(u_lightDirection, normal) * .5 + .5; + outColor = vec4(u_diffuse.rgb * fakeLight, u_diffuse.a); + } +`; +``` + +Затем, используя код из статьи о +[меньше кода больше веселья](webgl-less-code-more-fun.html) +сначала мы загрузим наши данные + +```js +async function main() { + // Получаем WebGL контекст + /** @type {HTMLCanvasElement} */ + const canvas = document.querySelector("#canvas"); + const gl = canvas.getContext("webgl2"); + if (!gl) { + return; + } + + // Говорим twgl сопоставить position с a_position и т.д.. + twgl.setAttributePrefix("a_"); + + ... шейдеры ... + + // компилирует и связывает шейдеры, ищет расположения атрибутов и uniform'ов + const meshProgramInfo = twgl.createProgramInfo(gl, [vs, fs]); + + const response = await fetch('resources/models/cube/cube.obj'); + const text = await response.text(); + const data = parseOBJ(text); + + // Потому что data - это просто именованные массивы, как этот + // + // { + // position: [...], + // texcoord: [...], + // normal: [...], + // } + // + // и потому что эти имена соответствуют атрибутам в нашем вершинном + // шейдере, мы можем передать это напрямую в `createBufferInfoFromArrays` + // из статьи "меньше кода больше веселья". + + // создаем буфер для каждого массива, вызывая + // gl.createBuffer, gl.bindBuffer, gl.bufferData + const bufferInfo = webglUtils.createBufferInfoFromArrays(gl, data); + // заполняет вершинный массив, вызывая gl.createVertexArray, gl.bindVertexArray + // затем gl.bindBuffer, gl.enableVertexAttribArray, и gl.vertexAttribPointer для каждого атрибута + const vao = twgl.createVAOFromBufferInfo(gl, meshProgramInfo, bufferInfo); +``` + +и затем мы нарисуем это + +```js + const cameraTarget = [0, 0, 0]; + const cameraPosition = [0, 0, 4]; + const zNear = 0.1; + const zFar = 50; + + function degToRad(deg) { + return deg * Math.PI / 180; + } + + function render(time) { + time *= 0.001; // конвертируем в секунды + + twgl.resizeCanvasToDisplaySize(gl.canvas); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.CULL_FACE); + + const fieldOfViewRadians = degToRad(60); + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const projection = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar); + + const up = [0, 1, 0]; + // Вычисляем матрицу камеры, используя look at. + const camera = m4.lookAt(cameraPosition, cameraTarget, up); + + // Делаем view матрицу из матрицы камеры. + const view = m4.inverse(camera); + + const sharedUniforms = { + u_lightDirection: m4.normalize([-1, 3, 5]), + u_view: view, + u_projection: projection, + }; + + gl.useProgram(meshProgramInfo.program); + + // вызывает gl.uniform + twgl.setUniforms(meshProgramInfo, sharedUniforms); + + // устанавливаем атрибуты для этой части. + gl.bindVertexArray(vao); + + // вызывает gl.uniform + twgl.setUniforms(meshProgramInfo, { + u_world: m4.yRotation(time), + u_diffuse: [1, 0.7, 0.5, 1], + }); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, bufferInfo); + + requestAnimationFrame(render); + } + requestAnimationFrame(render); +} +``` + +{{{example url="../webgl-load-obj.html"}}} + +## Множество заметок + +### Загрузчик выше неполный + +Вы можете [прочитать больше о формате .obj](http://paulbourke.net/dataformats/obj/). +Есть тонны функций, которые код выше не поддерживает. Также код не был +протестирован на очень многих .obj файлах, поэтому, возможно, есть скрытые ошибки. Тем не менее, я +подозреваю, что большинство .obj файлов в интернете используют только функции, показанные выше, поэтому я подозреваю, +что это, вероятно, полезный пример. + +### Загрузчик не проверяет ошибки + +Пример: ключевое слово `vt` может иметь 3 значения на запись вместо только 2. 3 значения +были бы для 3D текстур, что не распространено, поэтому я не беспокоился. Если бы вы передали ему +файл с 3D текстурными координатами, вам пришлось бы изменить шейдеры для обработки 3D +текстур и код, который генерирует `WebGLBuffers` (вызывает `createBufferInfoFromArrays`), +чтобы сказать ему, что это 3 компонента на UV координату. + +### Он предполагает, что данные однородны + +Я не знаю, могут ли некоторые ключевые слова `f` иметь 3 записи, +а другие только 2 в том же файле. Если это возможно, код выше не +обрабатывает это. + +Код также предполагает, что если позиции вершин имеют x, y, z, они все +имеют x, y, z. Если есть файлы, где некоторые позиции вершин +имеют x, y, z, другие имеют только x, y, а третьи имеют x, y, z, r, g, b, +тогда нам пришлось бы рефакторить. + +### Вы могли бы поместить все данные в один буфер + +Код выше помещает данные для позиции, текстурной координаты, нормали в отдельные буферы. +Вы могли бы поместить их в один буфер, либо перемешивая их +pos,uv,nrm,pos,uv,nrm,... но тогда вам нужно было бы изменить +то, как настроены атрибуты, чтобы передать strides и offsets. + +Расширяя это, вы могли бы даже поместить данные для всех частей в те же +буферы, где как в настоящее время это один буфер на тип данных на часть. + +Я оставил это, потому что не думаю, что это так важно, и потому что это загромоздило бы пример. + +### Вы могли бы переиндексировать вершины + +Код выше расширяет вершины в плоские списки треугольников. Мы могли бы переиндексировать +вершины. Особенно если мы поместим все данные вершин в один буфер или по крайней мере один +буфер на тип, но разделенный между частями, тогда в основном для каждого ключевого слова `f` вы конвертируете +индексы в положительные числа (переводите отрицательные числа в правильный положительный индекс), +и затем набор чисел является *id* для этой вершины. Так что вы можете хранить *карту id к индексу* +для помощи в поиске индексов. + +```js +const idToIndexMap = {} +const webglIndices = []; + +function addVertex(vert) { + const ptn = vert.split('/'); + // сначала конвертируем все индексы в положительные индексы + const indices = ptn.forEach((objIndexStr, i) => { + if (!objIndexStr) { + return; + } + const objIndex = parseInt(objIndexStr); + return objIndex + (objIndex >= 0 ? 0 : objVertexData[i].length); + }); + // теперь посмотрим, что эта конкретная комбинация позиции,текстурной координаты,нормали + // уже существует + const id = indices.join(','); + let vertIndex = idToIndexMap[id]; + if (!vertIndex) { + // Нет. Добавляем это. + vertIndex = webglVertexData[0].length / 3; + idToIndexMap[id] = vertexIndex; + indices.forEach((index, i) => { + if (index !== undefined) { + webglVertexData[i].push(...objVertexData[i][index]); + } + } + } + webglIndices.push(vertexIndex); +} +``` + +Или вы могли бы просто вручную переиндексировать, если думаете, что это важно. + +### Код не обрабатывает только позиции или только позиции + текстурные координаты. + +Код, как написано, предполагает, что нормали существуют. Как мы делали для +[примера с токарным станком](webgl-3d-geometry-lathe.html), мы могли бы генерировать нормали, +если они не существуют, принимая во внимание группы сглаживания, если мы хотим. Или мы +могли бы также использовать разные шейдеры, которые либо не используют нормали, либо вычисляют нормали. + +### Вы не должны использовать .OBJ файлы + +Честно говоря, вы не должны использовать .OBJ файлы, по моему мнению. Я в основном написал это как пример. +Если вы можете извлечь данные вершин из файла, вы можете написать импортеры для любого формата. + +Проблемы с .OBJ файлами включают + +* нет поддержки для света или камер + + Это может быть нормально, потому что, возможно, вы загружаете кучу частей + (как деревья, кусты, камни для ландшафта), и вам не нужны камеры + или свет. Тем не менее, приятно иметь опцию, если вы хотите загрузить целые сцены + как их создал какой-то художник. + +* Нет иерархии, Нет графа сцены + + Если вы хотите загрузить машину, идеально вы хотели бы иметь возможность поворачивать колеса + и иметь их вращение вокруг их центров. Это невозможно с .OBJ, + потому что .OBJ не содержит [граф сцены](webgl-scene-graph.html). Лучшие форматы + включают эти данные, что намного более полезно, если вы хотите иметь возможность ориентировать + части, сдвинуть окно, открыть дверь, двигать ноги персонажа и т.д... + +* нет поддержки для анимации или скиннинга + + Мы прошли [скиннинг](webgl-skinning.html) в другом месте, но .OBJ не предоставляет + данных для скиннинга и нет данных для анимации. Снова это может быть нормально + для ваших потребностей, но я бы предпочел один формат, который обрабатывает больше. + +* .OBJ не поддерживает более современные материалы. + + Материалы обычно довольно специфичны для движка, но в последнее время есть по крайней мере + некоторое соглашение о физически основанных рендеринговых материалах. .OBJ не поддерживает + это, насколько я знаю. + +* .OBJ требует парсинга + + Если вы не делаете универсальный просмотрщик для пользователей, чтобы загружать .OBJ файлы в него, + лучшая практика - использовать формат, который требует как можно меньше парсинга. + .GLTF - это формат, разработанный для WebGL. Он использует JSON, поэтому вы можете просто загрузить его. + Для бинарных данных он использует форматы, которые готовы загружаться в GPU напрямую, + нет необходимости парсить числа в массивы большую часть времени. + + Вы можете увидеть пример загрузки .GLTF файла в [статье о скиннинге](webgl-skinning.html). + + Если у вас есть .OBJ файлы, которые вы хотите использовать, лучшая практика - конвертировать их + в какой-то другой формат сначала, офлайн, а затем использовать лучший формат на вашей странице. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-matrix-naming.md b/webgl/lessons/ru/webgl-matrix-naming.md new file mode 100644 index 000000000..7cdecde28 --- /dev/null +++ b/webgl/lessons/ru/webgl-matrix-naming.md @@ -0,0 +1,69 @@ +Title: WebGL2 Именование матриц +Description: Общие имена для матриц +TOC: 3D - Именование матриц + + +Этот пост является продолжением серии постов о WebGL. Первый +[начался с основ](webgl-fundamentals.html), а предыдущий +был [о 3D камерах](webgl-3d-camera.html). + +Как весь сайт указал, практически все в WebGL +на 100% зависит от вас. За исключением нескольких предопределенных имен, как `gl_Position`, +почти все в WebGL определяется вами, программистом. + +Тем не менее, есть некоторые общие или полу-общие соглашения об именовании. Особенно +когда речь идет о матрицах. Я не знаю, кто первым придумал эти имена. Я +думаю, я узнал их [из Standard Annotations and Semantics от NVidia](https://www.nvidia.com/object/using_sas.html). +Это немного более формально, так как это был способ попытаться заставить шейдеры работать +в большем количестве ситуаций, решив на конкретные имена. Это немного устарело, +но основы все еще существуют. + +Вот список из моей головы + +* мировая матрица (или иногда матрица модели) + + матрица, которая берет вершины модели и перемещает их в мировое пространство + +* матрица камеры + + матрица, которая позиционирует камеру в мире. Другой способ сказать + это - это *мировая матрица* для камеры. + +* матрица вида + + матрица, которая перемещает все остальное в мире перед камерой. + Это обратная матрица *матрицы камеры*. + +* матрица проекции + + матрица, которая конвертирует усеченную пирамиду пространства в пространство отсечения или некоторое ортографическое + пространство в пространство отсечения. Другой способ думать об этом - это матрица, + возвращаемая функцией `perspective` и/или `ortho` или + `orthographic` вашей математической библиотеки матриц. + +* локальная матрица + + при использовании [графа сцены](webgl-scene-graph.html) локальная матрица - это + матрица в любом конкретном узле графа перед умножением с любыми другими + узлами. + + +Если шейдеру нужна комбинация этих, они обычно перечисляются справа налево, +хотя в шейдере они будут умножаться *справа*. Например: + + worldViewProjection = projection * view * world + +Две другие общие вещи, которые делают с матрицей - это взять обратную + + viewMatrix = inverse(cameraMatrix) + +И транспонировать + + worldInverseTranspose = transpose(inverse(world)) + +Надеюсь, зная эти термины, вы можете посмотреть на чужой шейдер +и если вам повезет, они использовали имена, которые близки или похожи на +эти. Тогда вы можете надеяться вывести, что эти шейдеры +фактически делают. + +Теперь давайте [изучим анимацию дальше](webgl-animation.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-matrix-vs-math.md b/webgl/lessons/ru/webgl-matrix-vs-math.md new file mode 100644 index 000000000..9623a88c1 --- /dev/null +++ b/webgl/lessons/ru/webgl-matrix-vs-math.md @@ -0,0 +1,199 @@ +Title: WebGL2 Матрицы против математических матриц +Description: Разница между соглашениями WebGL и математическими соглашениями. +TOC: WebGL2 Матрицы против математических матриц + + +Эта статья является отступлением от различных статей, которые говорят о +матрицах, в частности [статьи, которая вводит матрицы](webgl-2d-matrices.html), но также +[статьи, которая вводит 3D](webgl-3d-orthographic.html), [статьи о перспективной проекции](webgl-3d-perspective.html), +и [статьи о камерах](webgl-3d-camera.html). + +В программировании в общем строка идет слева направо, столбец идет вверх и вниз. + +> ## стол·бец +> /ˈkäləm/ +> +> *существительное* +> 1. вертикальная колонна, обычно цилиндрическая и сделанная из камня или + бетона, поддерживающая антаблемент, арку или другую структуру или стоящая отдельно как памятник. +> +> *синонимы*: колонна, столб, шест, вертикаль, ... +> +> 2. вертикальное деление страницы или текста. + +> ## строка +> /rō/ +> +> *существительное* +> * горизонтальная линия записей в таблице. + +Мы можем видеть примеры в нашем программном обеспечении. Например, мои текстовые редакторы +показывают Строки и столбцы, строки являются другим словом для строки в этом случае, поскольку столбец уже занят + +
+ +Обратите внимание в нижней левой области строка состояния показывает строку и столбец. + +В программном обеспечении для работы с электронными таблицами мы видим, что строки идут поперек + +
+ +А столбцы идут вниз. + +
+ +Итак, когда мы создаем матрицу 3x3 или 4x4 в JavaScript для WebGL, мы делаем их так + +```js +const m3x3 = [ + 0, 1, 2, // строка 0 + 3, 4, 5, // строка 1 + 6, 7, 8, // строка 2 +]; + +const m4x4 = [ + 0, 1, 2, 3, // строка 0 + 4, 5, 6, 7, // строка 1 + 8, 9, 10, 11, // строка 2 + 12, 13, 14, 15, // строка 3 +]; +``` + +Ясно следуя соглашениям выше, первая строка `m3x3` - это `0, 1, 2`, а последняя строка `m4x4` - это `12, 13, 14, 15` + +Как мы видим в [первой статье о матрицах](webgl-2d-matrices.html), чтобы сделать довольно стандартную WebGL 3x3 2D матрицу перевода, значения перевода `tx` и `ty` идут в позиции 6 и 7 + +```js +const some3x3TranslationMatrix = [ + 1, 0, 0, + 0, 1, 0, + tx, ty, 1, +]; +``` + +или для матрицы 4x4, которая вводится в [первой статье о 3D](webgl-3d-orthographic.html), перевод идет в позиции 12, 13, 14, как в + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1, +]; +``` + +Но есть проблема. Математические соглашения для матричной математики обычно делают вещи в столбцах. Математик написал бы матрицу перевода 3x3 так + +
+ +и матрицу перевода 4x4 так + +
+ +Это оставляет нас с проблемой. Если мы хотим, чтобы наши матрицы выглядели как +математические матрицы, мы можем решить написать матрицу 4x4 так + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, tx, + 0, 1, 0, ty, + 0, 0, 1, tx, + 0, 0, 0, 1, +]; +``` + +К сожалению, делать это так имеет проблемы. Как упоминалось в [статье о камерах](webgl-3d-camera.html), каждый из столбцов матрицы 4x4 часто имеет значение. + +Первый, второй и третий столбцы часто считаются осями x, y и z соответственно, а последний столбец - это позиция или перевод. + +Одна проблема в том, что в коде было бы не весело пытаться получить эти части +отдельно. Хотите ось Z? Вам пришлось бы делать это + +```js +const zAxis = [ + some4x4Matrix[2], + some4x4Matrix[6], + some4x4Matrix[10], +]; +``` + +Ух! + +Итак, способ, которым WebGL, и OpenGL ES, на котором основан WebGL, обходит это, заключается в том, что он называет строки "столбцами". + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, 0, // это столбец 0 + 0, 1, 0, 0, // это столбец 1 + 0, 0, 1, 0, // это столбец 2 + tx, ty, tz, 1, // это столбец 3 +]; +``` + +Теперь это соответствует математическому определению. Сравнивая с примером выше, если мы хотим ось Z, все, что нам нужно сделать, это + +```js +const zAxis = some4x4Matrix.slice(8, 11); +``` + +Для тех, кто знаком с C++, сам OpenGL требует, чтобы 16 значений матрицы 4x4 были последовательными в памяти, поэтому в C++ мы могли бы создать структуру или класс `Vec4` + +```c++ +// C++ +struct Vec4 { + float x; + float y; + float z; + float w; +}; +``` + +и мы могли бы создать матрицу 4x4 из 4 из них + +```c++ +// C++ +struct Mat4x4 { + Vec4 x_axis; + Vec4 y_axis; + Vec4 z_axis; + Vec4 translation; +} +``` + +или просто + +```c++ +// C++ +struct Mat4x4 { + Vec4 column[4]; +} +``` + +И это просто казалось бы работающим. + +К сожалению, это выглядит совсем не как математическая версия, когда вы фактически объявляете одну статически в коде. + +```C++ +// C++ +Mat4x4 someTranslationMatrix = { + { 1, 0, 0, 0, }, + { 0, 1, 0, 0, }, + { 0, 0, 1, 0, }, + { tx, ty, tz, 1, }, +}; +``` + +Или обратно к JavaScript, где у нас обычно нет чего-то вроде C++ структур. + +```js +const someTranslationMatrix = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1, +]; +``` + +Итак, с этим соглашением называть строки "столбцами" некоторые вещи проще, но другие могут быть более запутанными, если вы математик. + +Я поднимаю все это, потому что эти статьи написаны с точки зрения программиста, а не математика. Это означает, что как и каждый другой одномерный массив, который обрабатывается как двумерный массив, строки идут поперек. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-multiple-views.md b/webgl/lessons/ru/webgl-multiple-views.md new file mode 100644 index 000000000..67a47c58f --- /dev/null +++ b/webgl/lessons/ru/webgl-multiple-views.md @@ -0,0 +1,200 @@ +Title: WebGL2 Множественные виды, множественные canvas +Description: Рисование множественных видов +TOC: Множественные виды, множественные canvas + +Эта статья предполагает, что вы прочитали статью о +[меньше кода больше веселья](webgl-less-code-more-fun.html), +поскольку она использует библиотеку, упомянутую там, чтобы +не загромождать пример. Если вы не понимаете, +что такое буферы, вершинные массивы и атрибуты, или когда +функция с именем `twgl.setUniforms` что это означает +устанавливать uniform'ы, и т.д... тогда вам, вероятно, стоит пойти дальше назад и +[прочитать основы](webgl-fundamentals.html). + +Допустим, вы хотели нарисовать множественные виды +той же сцены, как мы могли бы это сделать? Один способ был бы +[рендерить в текстуры](webgl-render-to-texture.html), +а затем рисовать эти текстуры на canvas. Это +определенно правильный способ сделать это, и есть времена, когда это +может быть правильной вещью для делания. Но это требует, чтобы мы +выделили текстуры, отрендерили вещи в них, затем отрендерили +эти текстуры на canvas. Это означает, что мы эффективно +двойной рендеринг. Это может быть подходящим, например, +в гоночной игре, когда мы хотим отрендерить вид в зеркале +заднего вида, мы бы отрендерили то, что позади машины, в текстуру, +затем использовали бы эту текстуру для рисования зеркала заднего вида. + +Другой способ - это установить viewport и включить scissor тест. +Это отлично для ситуаций, где наши виды не перекрываются. Еще +лучше нет двойного рендеринга, как в решении выше. + +В [самой первой статье](webgl-fundamentals.html) упоминается, +что мы устанавливаем, как WebGL конвертирует из пространства отсечения в пространство пикселей, вызывая + +```js +gl.viewport(left, bottom, width, height); +``` + +Самая распространенная вещь - это установить их в `0`, `0`, `gl.canvas.width` и `gl.canvas.height` +соответственно, чтобы покрыть весь canvas. + +Вместо этого мы можем установить их в часть canvas, и они сделают так, +что мы будем рисовать только в этой части canvas. +WebGL обрезает вершины в пространстве отсечения. +Как мы упоминали раньше, мы устанавливаем `gl_Position` в нашем вершинном шейдере в значения, которые идут от -1 до +1 в x, y, z. +WebGL обрезает треугольники и линии, которые мы передаем, к этому диапазону. После того, как происходит обрезка, затем +применяются настройки `gl.viewport`, так что, например, если мы использовали + +```js +gl.viewport( + 10, // left + 20, // bottom + 30, // width + 40, // height +); +``` + +Тогда значение пространства отсечения x = -1 соответствует пикселю x = 10, а значение пространства отсечения ++1 соответствует пикселю x = 40 (left 10 плюс width 30) +(На самом деле это небольшое упрощение, [см. ниже](#pixel-coords)) + +Итак, после обрезки, если мы рисуем треугольник, он появится, чтобы поместиться внутри viewport. + +Давайте нарисуем нашу 'F' из [предыдущих статей](webgl-3d-perspective.html). + +Вершинный и фрагментный шейдеры те же, что используются в статьях о +[ортографической](webgl-3d-orthographic.html) и [перспективной](webgl-3d-perspective.html) +проекции. + +```glsl +#version 300 es +// вершинный шейдер +in vec4 a_position; +in vec4 a_color; + +uniform mat4 u_matrix; + +out vec4 v_color; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_matrix * a_position; + + // Передаем цвет вершины в фрагментный шейдер. + v_color = a_color; +} +``` + +```glsl +#version 300 es +// фрагментный шейдер +precision highp float; + +// Передается из вершинного шейдера. +in vec4 v_color; + +out vec4 outColor; + +void main() { + outColor = v_color; +} +``` + +Затем во время инициализации нам нужно создать программу и +буферы и вершинный массив для 'F' + +```js +// настройка GLSL программ +// компилирует шейдеры, связывает программу, ищет расположения +const programInfo = twgl.createProgramInfo(gl, [vs, fs]); + +// Говорим twgl сопоставить position с a_position, +// normal с a_normal и т.д.. +twgl.setAttributePrefix("a_"); + +// создаем буферы и заполняем данными для 3D 'F' +const bufferInfo = twgl.primitives.create3DFBufferInfo(gl); +const vao = twgl.createVAOFromBufferInfo(gl, programInfo, bufferInfo); +``` + +И для рисования давайте сделаем функцию, которой мы можем передать матрицу проекции, +матрицу камеры и мировую матрицу + +```js +function drawScene(projectionMatrix, cameraMatrix, worldMatrix) { + // Делаем view матрицу из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + + let mat = m4.multiply(projectionMatrix, viewMatrix); + mat = m4.multiply(mat, worldMatrix); + + gl.useProgram(programInfo.program); + + // ------ Рисуем F -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(vao); + + // Устанавливаем uniform'ы + twgl.setUniforms(programInfo, { + u_matrix: mat, + }); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, bufferInfo); +} +``` + +и затем давайте вызовем эту функцию, чтобы нарисовать F. + +```js +function degToRad(d) { + return d * Math.PI / 180; +} + +const settings = { + rotation: 150, // в градусах +}; +const fieldOfViewRadians = degToRad(120); + +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, near, far); + + // Вычисляем матрицу камеры, используя look at. + const cameraPosition = [0, 0, -75]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // поворачиваем F в мировом пространстве + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг его начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); +} +render(); +``` + +Это в основном то же самое, что и финальный пример из +[статьи о перспективе](webgl-3d-perspective.html), +за исключением того, что мы используем [нашу библиотеку](webgl-less-code-more-fun.html), чтобы держать код проще. + +{{{example url="../webgl-multiple-views-one-view.html"}}} + +Теперь давайте сделаем так, чтобы он рисовал 2 вида 'F' бок о бок, +используя `gl.viewport` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-picking.md b/webgl/lessons/ru/webgl-picking.md new file mode 100644 index 000000000..fdb830946 --- /dev/null +++ b/webgl/lessons/ru/webgl-picking.md @@ -0,0 +1,182 @@ +Title: WebGL2 Пикинг (выбор объектов) +Description: Как выбирать объекты в WebGL +TOC: Пикинг (клик по объектам) + +Эта статья о том, как использовать WebGL, чтобы позволить пользователю выбирать или выделять объекты. + +Если вы читали другие статьи на этом сайте, вы, вероятно, уже поняли, +что сам WebGL — это просто библиотека растеризации. Он рисует треугольники, +линии и точки на canvas, поэтому у него нет понятия «объекты для выбора». +Он просто выводит пиксели через ваши шейдеры. Это значит, +что любая концепция «пикинга» должна реализовываться в вашем коде. Вы должны +определить, что это за объекты, которые пользователь может выбрать. +То есть, хотя эта статья может охватить общие концепции, вам нужно будет +самостоятельно решить, как применить их в вашем приложении. + +## Клик по объекту + +Один из самых простых способов определить, по какому объекту кликнул пользователь — +присвоить каждому объекту числовой id, затем отрисовать +все объекты, используя их id как цвет, без освещения +и текстур. Это даст нам изображение силуэтов +каждого объекта. Буфер глубины сам отсортирует объекты. +Затем мы можем считать цвет пикселя под мышью — это даст нам id объекта, который был отрисован в этой точке. + +Чтобы реализовать этот метод, нам нужно объединить несколько предыдущих +статей. Первая — [о рисовании множества объектов](webgl-drawing-multiple-things.html), +потому что она показывает, как рисовать много объектов, которые мы и будем выбирать. + +Обычно мы хотим рендерить эти id вне экрана — +[рендеря в текстуру](webgl-render-to-texture.html), так что добавим и этот код. + +Начнем с последнего примера из +[статьи о рисовании множества объектов](webgl-drawing-multiple-things.html), +который рисует 200 объектов. + +Добавим к нему framebuffer с прикреплённой текстурой и depth-буфером из +последнего примера в [статье о рендере в текстуру](webgl-render-to-texture.html). + +```js +// Создаем текстуру для рендера +const targetTexture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, targetTexture); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + +// создаем depth renderbuffer +const depthBuffer = gl.createRenderbuffer(); +gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); + +function setFramebufferAttachmentSizes(width, height) { + gl.bindTexture(gl.TEXTURE_2D, targetTexture); + // задаем размер и формат уровня 0 + const level = 0; + const internalFormat = gl.RGBA; + const border = 0; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; + const data = null; + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, + width, height, border, + format, type, data); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); +} + +// Создаем и биндим framebuffer +const fb = gl.createFramebuffer(); +gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + +// прикрепляем текстуру как первый цветовой attachment +const attachmentPoint = gl.COLOR_ATTACHMENT0; +const level = 0; +gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level); + +// создаем depth-буфер такого же размера, как targetTexture +gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); +``` + +Мы вынесли код установки размеров текстуры и depth renderbuffer в функцию, чтобы +можно было вызывать её при изменении размера canvas. + +В рендер-цикле, если размер canvas изменился, +мы подгоним текстуру и renderbuffer под новые размеры. + +```js +function drawScene(time) { + time *= 0.0005; + +- webglUtils.resizeCanvasToDisplaySize(gl.canvas); ++ if (webglUtils.resizeCanvasToDisplaySize(gl.canvas)) { ++ // canvas изменился, подгоняем framebuffer attachments ++ setFramebufferAttachmentSizes(gl.canvas.width, gl.canvas.height); ++ } + +... +``` + +Далее нам нужен второй шейдер. В примере используется рендер по цветам вершин, но нам нужен +шейдер, который будет рисовать сплошным цветом (id). +Вот наш второй шейдер: + +```js +const pickingVS = `#version 300 es + in vec4 a_position; + + uniform mat4 u_matrix; + + void main() { + // Умножаем позицию на матрицу. + gl_Position = u_matrix * a_position; + } +`; + +const pickingFS = `#version 300 es + precision highp float; + + uniform vec4 u_id; + + out vec4 outColor; + + void main() { + outColor = u_id; + } +`; +``` + +И нам нужно скомпилировать, связать и найти локации +используя наши [хелперы](webgl-less-code-more-fun.html). + +```js +// настройка GLSL программ +// важно: нам нужно, чтобы атрибуты совпадали между программами +// чтобы можно было использовать один и тот же vertex array для разных шейдеров +const options = { + attribLocations: { + a_position: 0, + a_color: 1, + }, +}; +const programInfo = twgl.createProgramInfo(gl, [vs, fs], options); +const pickingProgramInfo = twgl.createProgramInfo(gl, [pickingVS, pickingFS], options); +``` + +В отличие от большинства примеров на сайте, здесь нам нужно рисовать одни и те же данные двумя разными шейдерами. +Поэтому нам нужно, чтобы локации атрибутов совпадали между шейдерами. Это можно сделать двумя способами. Первый — явно указать их в GLSL: + +```glsl +layout (location = 0) in vec4 a_position; +layout (location = 1) in vec4 a_color; +``` + +Второй — вызвать `gl.bindAttribLocation` **до** линковки программы: + +```js +gl.bindAttribLocation(someProgram, 0, 'a_position'); +gl.bindAttribLocation(someProgram, 1, 'a_color'); +gl.linkProgram(someProgram); +``` + +Этот способ нечасто используется, но он более +[D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). +Наша хелпер-библиотека вызывает `gl.bindAttribLocation` за нас, +если мы передаем имена атрибутов и нужные локации — это и происходит выше. + +Это гарантирует, что атрибут `a_position` будет использовать локацию 0 в обеих программах, так что мы можем использовать один и тот же vertex array. + +Далее нам нужно уметь рендерить все объекты дважды: сначала обычным шейдером, потом — только что написанным. +Вынесем код рендера всех объектов в функцию. + +```js +function drawObjects(objectsToDraw, overrideProgramInfo) { + objectsToDraw.forEach(function(object) { + const programInfo = overrideProgramInfo || object.programInfo; + const bufferInfo = object.bufferInfo; + const vertexArray = object.vertexArray; + + gl.useProgram(programInfo.program); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(vertexArray); \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-planar-projection-mapping.md b/webgl/lessons/ru/webgl-planar-projection-mapping.md new file mode 100644 index 000000000..26a01d169 --- /dev/null +++ b/webgl/lessons/ru/webgl-planar-projection-mapping.md @@ -0,0 +1,195 @@ +Title: WebGL2 Планарное и перспективное проекционное отображение +Description: Проецирование текстуры как плоскости +TOC: Планарное и перспективное проекционное отображение + +В этой статье предполагается, что вы уже прочитали статью +[меньше кода — больше удовольствия](webgl-less-code-more-fun.html), +так как здесь используется упомянутая там библиотека для +упрощения примера. Если вы не понимаете, что такое буферы, вершинные массивы, атрибуты или +что значит функция `twgl.setUniforms`, как устанавливать uniforms и т.д., +то вам стоит вернуться и [прочитать основы](webgl-fundamentals.html). + +Также предполагается, что вы прочитали [статьи о перспективе](webgl-3d-perspective.html), +[статью о камерах](webgl-3d-camera.html), [статью о текстурах](webgl-3d-textures.html) +и [статью о визуализации камеры](webgl-visualizing-the-camera.html), +поэтому если вы их не читали, начните с них. + +Проекционное отображение — это процесс «проецирования» изображения, как если бы вы +направили кинопроектор на экран и спроецировали на него фильм. +Кинопроектор проецирует перспективную плоскость. Чем дальше экран от проектора, +тем больше изображение. Если наклонить экран, чтобы он был не перпендикулярен проектору, +получится трапеция или произвольный четырёхугольник. + +
+ +Конечно, проекционное отображение не обязательно должно быть плоским. Существуют +цилиндрические, сферические и другие виды проекционного отображения. + +Сначала рассмотрим планарное проекционное отображение. В этом случае +можно представить, что проектор такого же размера, как и экран, +поэтому изображение не увеличивается с удалением экрана от проектора, а остаётся одного размера. + +
+ +Для начала создадим простую сцену с плоскостью и сферой. +Мы наложим на обе объекты простую 8x8 текстуру в виде шахматной доски. + +Шейдеры похожи на те, что были в [статье о текстурах](webgl-3d-textures.html), +только матрицы разделены, чтобы не умножать их в JavaScript. + +```js +const vs = `#version 300 es +in vec4 a_position; +in vec2 a_texcoord; + +uniform mat4 u_projection; +uniform mat4 u_view; +uniform mat4 u_world; + +out vec2 v_texcoord; + +void main() { + gl_Position = u_projection * u_view * u_world * a_position; + + // Передаём текстурные координаты во фрагментный шейдер. + v_texcoord = a_texcoord; +} +`; +``` + +Также я добавил uniform `u_colorMult`, чтобы умножать цвет текстуры. +Используя монохромную текстуру, мы можем менять её цвет таким образом. + +```js +const fs = `#version 300 es +precision highp float; + +// Передано из вершинного шейдера. +in vec2 v_texcoord; + +uniform vec4 u_colorMult; +uniform sampler2D u_texture; + +out vec4 outColor; + +void main() { + outColor = texture(u_texture, v_texcoord) * u_colorMult; +} +`; +``` + +Вот код для настройки программы, буферов сферы и плоскости: + +```js +// настройка GLSL программы +// компиляция шейдеров, линковка программы, поиск локаций +const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs]); + +const sphereBufferInfo = primitives.createSphereBufferInfo( + gl, + 1, // радиус + 12, // делений по кругу + 6, // делений по высоте +); +const sphereVAO = twgl.createVAOFromBufferInfo( + gl, textureProgramInfo, sphereBufferInfo); +const planeBufferInfo = primitives.createPlaneBufferInfo( + gl, + 20, // ширина + 20, // высота + 1, // делений по ширине + 1, // делений по высоте +); +const planeVAO = twgl.createVAOFromBufferInfo( + gl, textureProgramInfo, planeBufferInfo); +``` + +и код для создания 8x8 текстуры-шахматки +(см. [статью о data-текстурах](webgl-data-textures.html)): + +```js +// создаём 8x8 текстуру-шахматку +const checkerboardTexture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, checkerboardTexture); +gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip уровень + gl.LUMINANCE, // внутренний формат + 8, // ширина + 8, // высота + 0, // граница + gl.LUMINANCE, // формат + gl.UNSIGNED_BYTE, // тип + new Uint8Array([ // данные + 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, + 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, + 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, + 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, + 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, + 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, + 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, + 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, + ])); +gl.generateMipmap(gl.TEXTURE_2D); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +``` + +Для рендера создадим функцию, которая принимает матрицу проекции +и матрицу камеры, вычисляет view-матрицу из матрицы камеры, +а затем рисует сферу и плоскость: + +```js +// Uniforms для каждого объекта. +const planeUniforms = { + u_colorMult: [0.5, 0.5, 1, 1], // светло-голубой + u_texture: checkerboardTexture, + u_world: m4.translation(0, 0, 0), +}; +const sphereUniforms = { + u_colorMult: [1, 0.5, 0.5, 1], // розовый + u_texture: checkerboardTexture, + u_world: m4.translation(2, 3, 4), +}; + +function drawScene(projectionMatrix, cameraMatrix) { + // Получаем view-матрицу из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + + gl.useProgram(textureProgramInfo.program); + + // Устанавливаем uniforms, общие для сферы и плоскости + twgl.setUniforms(textureProgramInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, + }); + + // ------ Рисуем сферу -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(sphereVAO); + + // Устанавливаем uniforms, уникальные для сферы + twgl.setUniforms(textureProgramInfo, sphereUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, sphereBufferInfo); + + // ------ Рисуем плоскость -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(planeVAO); + + // Устанавливаем uniforms, уникальные для плоскости + twgl.setUniforms(textureProgramInfo, planeUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, planeBufferInfo); +} +``` + +Эту функцию можно вызывать из функции `render` примерно так: + +```js +const settings = { + cameraX: 2.75, + cameraY: 5, \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-points-lines-triangles.md b/webgl/lessons/ru/webgl-points-lines-triangles.md new file mode 100644 index 000000000..fc0962c4c --- /dev/null +++ b/webgl/lessons/ru/webgl-points-lines-triangles.md @@ -0,0 +1,131 @@ +Title: WebGL2 Точки, линии и треугольники +Description: Детали рисования точек, линий и треугольников +TOC: Точки, линии и треугольники + +Большинство этого сайта рисует все +треугольниками. Это аргументированно нормальная вещь, +которую делают 99% WebGL программ. Но, ради +полноты, давайте рассмотрим несколько других случаев. + +Как упоминалось в [первой статье](webgl-fundamentals.html), +WebGL рисует точки, линии и треугольники. Он делает это, +когда мы вызываем `gl.drawArrays` или `gl.drawElements`. +Мы предоставляем вершинному шейдеру координаты clip space, +и затем, на основе первого аргумента +к `gl.drawArrays` или `gl.drawElements`, WebGL будет +рисовать точки, линии или треугольники. + +Допустимые значения для первого аргумента `gl.drawArrays` +и `gl.drawElements`: + +* `POINTS` + + Для каждой вершины clip space, выводимой вершинным шейдером, рисуется квадрат, + центрированный над этой точкой. Размер квадрата + указывается установкой специальной переменной `gl_PointSize` + внутри вершинного шейдера в размер, который мы хотим для этого квадрата в пикселях. + + Примечание: Максимальный (и минимальный) размер, который может быть у этого квадрата, + зависит от реализации, что вы можете запросить с помощью + + const [minSize, maxSize] = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE); + + Также см. другую проблему [здесь](webgl-drawing-without-data.html#pointsissues). + +* `LINES` + + Для каждых 2 вершин clip space, выводимых вершинным шейдером, + рисуется линия, соединяющая 2 точки. Если у нас были точки A,B,C,D,E,F, то + мы получили бы 3 линии. + +
+ + Спецификация говорит, что мы можем установить толщину этой линии, + вызвав `gl.lineWidth` и указав ширину в пикселях. + В реальности, однако, максимальная + ширина зависит от реализации, и для большинства + реализаций максимальная ширина равна 1. + + const [minSize, maxSize] = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE); + + > Это в основном потому, что значения > 1 были устаревшими + в основном Desktop OpenGL. + +* `LINE_STRIP` + + Для каждой вершины clip space, выводимой вершинным шейдером, + рисуется линия от предыдущей точки, выводимой вершинным + шейдером. + + Итак, если вы выводите вершины clip space A,B,C,D,E,F, вы получите 5 линий. + +
+ +* `LINE_LOOP` + + Это то же самое, что и `LINE_STRIP`, но еще одна линия + рисуется от последней точки к первой точке. + +
+ +* `TRIANGLES` + + Для каждых 3 вершин clip space, выводимых вершинным шейдером, + рисуется треугольник из 3 точек. Это наиболее используемый режим. + +
+ +* `TRIANGLE_STRIP` + + Для каждой вершины clip space, выводимой вершинным шейдером, + рисуется треугольник из последних 3 вершин. Другими словами, + если вы выводите 6 точек A,B,C,D,E,F, то будет нарисовано + 4 треугольника. A,B,C затем B,C,D затем C,D,E затем D,E,F + +
+ +* `TRIANGLE_FAN` + + Для каждой вершины clip space, выводимой вершинным шейдером, + рисуется треугольник из первой вершины и последних 2 + вершин. Другими словами, если вы выводите 6 точек A,B,C,D,E,F, + то будет нарисовано 4 треугольника. A,B,C затем A,C,D затем + A,D,E и наконец A,E,F + +
+ +Я уверен, что некоторые другие не согласятся, но по моему опыту +`TRIANGLE_FAN` и `TRIANGLE_STRIP` лучше избегать. +Они подходят только для нескольких исключительных случаев, и дополнительный код +для обработки этих случаев не стоит того, чтобы просто делать все +треугольниками с самого начала. В частности, возможно, у вас +есть инструменты для построения нормалей или генерации координат текстуры +или выполнения любого другого количества вещей с данными вершин. Придерживаясь +только `TRIANGLES`, ваши функции просто будут работать. +Как только вы начнете добавлять `TRIANGLE_FAN` и `TRIANGLE_STRIP`, +вам понадобятся дополнительные функции для обработки большего количества случаев. +Вы свободны не соглашаться и делать все, что хотите. +Я просто говорю, что это мой опыт и опыт +нескольких AAA разработчиков игр, которых я спрашивал. + +Аналогично `LINE_LOOP` и `LINE_STRIP` не так полезны +и имеют аналогичные проблемы. +Как `TRIANGLE_FAN` и `TRIANGLE_STRIP`, ситуации +для их использования редки. Например, вы можете подумать, что +хотите нарисовать 4 соединенные линии, каждая из которых состоит из 4 точек. + +
+ +Если вы используете `LINE_STRIP`, вам нужно будет сделать 4 вызова к `gl.drawArrays` +и больше вызовов для настройки атрибутов для каждой линии, тогда как если вы +просто используете `LINES`, то вы можете вставить все точки, необходимые для рисования +всех 4 наборов линий одним вызовом к `gl.drawArrays`. Это будет +намного намного быстрее. + +Кроме того, `LINES` могут быть отличными для использования для отладки или простых +эффектов, но учитывая их ограничение в 1 пиксель ширины на большинстве платформ, +это часто неправильное решение. Если вы хотите нарисовать сетку для графика или +показать контуры многоугольников в программе 3D моделирования, использование `LINES` +может быть отличным, но если вы хотите нарисовать структурированную графику, такую как +SVG или Adobe Illustrator, то это не будет работать, и вам придется +[рендерить ваши линии каким-то другим способом, обычно из треугольников](https://mattdesl.svbtle.com/drawing-lines-is-hard). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-precision-issues.md b/webgl/lessons/ru/webgl-precision-issues.md new file mode 100644 index 000000000..d7e35546d --- /dev/null +++ b/webgl/lessons/ru/webgl-precision-issues.md @@ -0,0 +1,271 @@ +Title: WebGL2 Проблемы точности +Description: Проблемы точности в WebGL2 +TOC: Проблемы точности + +Эта статья о различных проблемах точности в WebGL2 + +## `lowp`, `mediump`, `highp` + +В [первой статье на этом сайте](webgl-fundamentals.html) мы создали +вершинный шейдер и фрагментный шейдер. Когда мы создавали фрагментный +шейдер, было упомянуто почти вскользь, что фрагментный шейдер +не имеет точности по умолчанию, и поэтому нам нужно было установить одну, добавив +строку + +```glsl +precision highp float; +``` + +Что это вообще было? + +`lowp`, `mediump` и `highp` - это настройки точности. Точность в данном случае +эффективно означает, сколько битов используется для хранения значения. Число в +JavaScript использует 64 бита. Большинство чисел в WebGL только 32 бита. Меньше битов = +быстрее, больше битов = более точно и/или больший диапазон. + +Я не знаю, смогу ли я объяснить это хорошо. Вы можете поискать +[double vs float](https://www.google.com/search?q=double+vs+float) +для других примеров проблем точности, но один способ объяснить это как разницу +между байтом и шортом или в JavaScript `Uint8Array` против +`Uint16Array`. + +* `Uint8Array` - это массив беззнаковых 8-битных целых чисел. 8 бит могут содержать 28 значений от 0 до 255. +* `Uint16Array` - это массив беззнаковых 16-битных целых чисел. 16 бит могут содержать 216 значений от 0 до 65535. +* `Uint32Array` - это массив беззнаковых 32-битных целых чисел. 32 бита могут содержать 232 значений от 0 до 4294967295. + +`lowp`, `mediump` и `highp` похожи. + +* `lowp` - это как минимум 9-битное значение. Для значений с плавающей точкой они могут варьироваться + от: -2 до +2, для целых значений они похожи на `Uint8Array` или `Int8Array` + +* `mediump` - это как минимум 16-битное значение. Для значений с плавающей точкой они могут варьироваться + от: -214 до +214, для целых значений они похожи на + `Uint16Array` или `Int16Array` + +* `highp` - это как минимум 32-битное значение. Для значений с плавающей точкой они могут варьироваться + от: -262 до +262, для целых значений они похожи на + `Uint32Array` или `Int32Array` + +Важно отметить, что не каждое значение внутри диапазона может быть представлено. +Самый простой для понимания, вероятно, `lowp`. Есть только 9 бит, и поэтому только +512 уникальных значений могут быть представлены. Выше говорится, что диапазон от -2 до +2, но +есть бесконечное количество значений между -2 и +2. Например, 1.9999999 +и 1.999998 - это 2 значения между -2 и +2. С только 9 битами `lowp` не может +представить эти 2 значения. Так, например, если вы хотите сделать какую-то математику с цветом и +вы использовали `lowp`, вы можете увидеть некоторую полосатость. Не вдаваясь в то, какие +фактические значения могут быть представлены, мы знаем, что цвета идут от 0 до 1. Если `lowp` +идет от -2 до +2 и может представлять только 512 уникальных значений, то кажется вероятным, +что только 128 из этих значений помещаются между 0 и 1. Это также предполагает, что если у вас есть +значение, которое составляет 4/128, и я пытаюсь добавить к нему 1/512, ничего не произойдет, +потому что 1/512 не может быть представлено `lowp`, поэтому это эффективно 0. + +Мы могли бы просто использовать `highp` везде и полностью игнорировать эту проблему, +но на устройствах, которые действительно используют 9 бит для `lowp` и/или 16 бит для +`mediump`, они обычно быстрее, чем `highp`. Часто значительно быстрее. + +К последнему пункту, в отличие от значений в `Uint8Array` или `Uint16Array`, значение `lowp` +или `mediump`, или, если на то пошло, даже значение `highp`, может использовать +более высокую точность (больше бит). Так, например, на настольном GPU, если вы поставите +`mediump` в ваш шейдер, он все равно, скорее всего, будет использовать 32 бита внутренне. Это +имеет проблему, что трудно тестировать ваши шейдеры, если вы используете `lowp` или +`mediump`. Чтобы увидеть, действительно ли ваши шейдеры работают правильно с `lowp` или +`mediump`, вы должны тестировать на устройстве, которое действительно использует 8 бит для `lowp` и +16 бит для `highp`. + +Если вы действительно хотите попытаться использовать `mediump` для скорости, вот некоторые из проблем, +которые возникают. + +Хороший пример, вероятно, пример [точечных источников света](webgl-3d-lighting-point.html), +в частности, вычисление зеркального блика, передает значения в мировом или видовом пространстве в фрагментный шейдер, +эти значения могут легко выйти за пределы диапазона для значения `mediump`. Так что, может быть, на +устройстве `mediump` вы могли бы просто убрать зеркальные блики. Например, вот +шейдер точечного света из [статьи о точечных источниках света](webgl-3d-lighting-point.html), +измененный для `mediump`. + +```glsl +#version 300 es + +-precision highp float; ++precision mediump float; + +// Переданный и измененный из вершинного шейдера. +in vec3 v_normal; +in vec3 v_surfaceToLight; +in vec3 v_surfaceToView; + +uniform vec4 u_color; +uniform float u_shininess; + +// нам нужно объявить выход для фрагментного шейдера +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 = 0.0; +- if (light > 0.0) { +- specular = pow(dot(normal, halfVector), u_shininess); +- } + + outColor = u_color; + + // Давайте умножим только цветовую часть (не альфа) + // на свет + outColor.rgb *= light; + +- // Просто добавляем блик +- outColor.rgb += specular; +} +``` + +Примечание: Даже этого на самом деле недостаточно. В вершинном шейдере у нас есть + +```glsl + // вычисляем вектор поверхности к свету + // и передаем его в фрагментный шейдер + v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition; +``` + +Так что скажем, свет находится на расстоянии 1000 единиц от поверхности. +Затем мы попадаем в фрагментный шейдер, и эта строка + +```glsl + vec3 surfaceToLightDirection = normalize(v_surfaceToLight); +``` + +выглядит достаточно невинно. За исключением того, что нормальный способ нормализовать вектор +- это разделить на его длину, а нормальный способ вычислить длину + +``` + float length = sqrt(v.x * v.x + v.y * v.y * v.z * v.z); +``` + +Если один из этих x, y или z равен 1000, то 1000*1000 = 1000000. 1000000 +выходит за пределы диапазона для `mediump`. + +Одно решение здесь - нормализовать в вершинном шейдере. + +``` + // вычисляем вектор поверхности к свету + // и передаем его в фрагментный шейдер +- v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition; ++ v_surfaceToLight = normalize(u_lightWorldPosition - surfaceWorldPosition); +``` + +Теперь значения, присвоенные `v_surfaceToLight`, находятся между -1 и +1, что +в пределах диапазона для `mediump`. + +Обратите внимание, что нормализация в вершинном шейдере на самом деле не даст +те же результаты, но они могут быть достаточно близкими, что никто не заметит, +если не сравнивать бок о бок. + +Функции, такие как `normalize`, `length`, `distance`, `dot`, все имеют эту +проблему, что если значения слишком большие, они выйдут за пределы диапазона +для `mediump`. + +Но вы действительно должны тестировать на устройстве, для которого `mediump` составляет 16 бит. +На настольном компьютере `mediump` составляет 32 бита, то же самое, что и `highp`, и поэтому любые проблемы +не будут видны. + +## Обнаружение поддержки 16-битного `mediump` + +Вы вызываете `gl.getShaderPrecisionFormat`, +вы передаете тип шейдера, `VERTEX_SHADER` или `FRAGMENT_SHADER`, и вы +передаете один из `LOW_FLOAT`, `MEDIUM_FLOAT`, `HIGH_FLOAT`, +`LOW_INT`, `MEDIUM_INT`, `HIGH_INT`, и он +[возвращает информацию о точности]. + +{{{example url="../webgl-precision-lowp-mediump-highp.html"}}} + +`gl.getShaderPrecisionFormat` возвращает объект с тремя значениями: `precision`, `rangeMin` и `rangeMax`. + +Для `LOW_FLOAT` и `MEDIUM_FLOAT` `precision` будет 23, если они действительно +просто `highp`. Иначе они, вероятно, будут 8 и 15 соответственно, или +по крайней мере они будут меньше 23. Для `LOW_INT` и `MEDIUM_INT` +если они такие же, как `highp`, то `rangeMin` будет 31. Если они +меньше 31, то `mediump int` на самом деле более эффективен, чем +`highp int`, например. + +Мой Pixel 2 XL использует 16 бит для `mediump`, он также использует 16 бит для `lowp`. Я не уверен, что когда-либо использовал устройство, которое использует 9 бит для `lowp`, поэтому я не уверен, какие проблемы обычно возникают, если они есть. + +На протяжении этих статей мы указывали точность по умолчанию +в фрагментном шейдере. Мы также можем указать точность любой отдельной +переменной. Например + +```glsl +uniform mediump vec4 color; // uniform +in lowp vec4 normal; // атрибут или varying вход +out lowp vec4 texcoord; // выход фрагментного шейдера или varying выход +lowp float foo; // переменная +``` + +## Форматы текстур + +Текстуры - это еще одно место, где спецификация говорит, что фактическая точность, +используемая, может быть больше, чем запрошенная точность. + +В качестве примера вы можете запросить 16-битную, 4-битную на канал текстуру, как это + +``` +gl.texImage2D( + gl.TEXTURE_2D, // target + 0, // mip level + gl.RGBA4, // internal format + width, // width + height, // height + 0, // border + gl.RGBA, // format + gl.UNSIGNED_SHORT_4_4_4_4, // type + null, +); +``` + +Но реализация может на самом деле использовать формат более высокого разрешения внутренне. +Я считаю, что большинство настольных компьютеров делают это, а большинство мобильных GPU - нет. + +Мы можем протестировать. Сначала мы запросим 4-битную на канал текстуру, как выше. +Затем мы [отрендерим в нее](webgl-render-to-texture.html), рендеря +некоторый градиент от 0 до 1. + +Затем мы отрендерим эту текстуру на холст. Если текстура действительно 4 бита +на канал внутренне, будет только 16 уровней цвета из градиента, +который мы нарисовали. Если текстура действительно 8 бит на канал, мы увидим 256 уровней +цветов. + +{{{example url="../webgl-precision-textures.html"}}} + +Запуская это на моем смартфоне, я вижу, что текстура использует 4 бита на канал +(или по крайней мере 4 бита в красном, поскольку я не тестировал другие каналы). + +
+ +Тогда как на моем настольном компьютере я могу видеть, что текстура на самом деле использует 8 бит на +канал, даже though я только запросил 4. + +
+ +Одна вещь для заметки в том, что по умолчанию WebGL может дизерить свои результаты, чтобы сделать +градации, как эта, выглядеть более гладкими. Вы можете выключить дизеринг с + +```js +gl.disable(gl.DITHER); +``` + +Если я не выключаю дизеринг, то мой смартфон производит это. + +
+ +Сходу единственное место, где это действительно возникло бы, это если бы вы +использовали некоторый формат текстуры с более низким битовым разрешением как цель рендеринга и не +тестировали на устройстве, где эта текстура действительно имеет это более низкое разрешение. +Если вы только тестировали на настольном компьютере, любые проблемы, которые это вызывает, могут не быть очевидными. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-pulling-vertices.md b/webgl/lessons/ru/webgl-pulling-vertices.md new file mode 100644 index 000000000..236166edb --- /dev/null +++ b/webgl/lessons/ru/webgl-pulling-vertices.md @@ -0,0 +1,198 @@ +Title: WebGL2 Вытягивание вершин +Description: Использование независимых индексов +TOC: Вытягивание вершин + +В этой статье предполагается, что вы уже прочитали многие другие статьи, +начиная с [основ](webgl-fundamentals.html). +Если вы их не читали, начните с них. + +Традиционно WebGL-приложения помещают геометрические данные в буферы. +Затем с помощью атрибутов эти данные автоматически подаются из буферов +в вершинный шейдер, где программист пишет код для преобразования их в clip space. + +Слово **традиционно** здесь важно. Это всего лишь **традиция** +делать так. Это вовсе не требование. WebGL не +заботится о том, как мы это делаем, ему важно только, чтобы наш вершинный шейдер +присваивал координаты clip space переменной `gl_Position`. + +Давайте нарисуем куб с текстурой, используя код, похожий на примеры из [статьи о текстурах](webgl-3d-textures.html). +Говорят, что нам нужно как минимум 24 уникальные вершины. Это потому, что, хотя у куба всего 8 угловых +позиций, один и тот же угол используется на 3 разных гранях, +и для каждой грани нужны свои текстурные координаты. + +
+ +На диаграмме выше видно, что для левой грани угол 3 требует +текстурных координат 1,1, а для правой грани тот же угол 3 требует +координат 0,1. Для верхней грани понадобятся ещё другие координаты. + +Обычно это реализуется так: из 8 угловых позиций +делают 24 вершины + +```js + // front + { pos: [-1, -1, 1], uv: [0, 1], }, // 0 + { pos: [ 1, -1, 1], uv: [1, 1], }, // 1 + { pos: [-1, 1, 1], uv: [0, 0], }, // 2 + { pos: [ 1, 1, 1], uv: [1, 0], }, // 3 + // right + { pos: [ 1, -1, 1], uv: [0, 1], }, // 4 + { pos: [ 1, -1, -1], uv: [1, 1], }, // 5 + { pos: [ 1, 1, 1], uv: [0, 0], }, // 6 + { pos: [ 1, 1, -1], uv: [1, 0], }, // 7 + // back + { pos: [ 1, -1, -1], uv: [0, 1], }, // 8 + { pos: [-1, -1, -1], uv: [1, 1], }, // 9 + { pos: [ 1, 1, -1], uv: [0, 0], }, // 10 + { pos: [-1, 1, -1], uv: [1, 0], }, // 11 + // left + { pos: [-1, -1, -1], uv: [0, 1], }, // 12 + { pos: [-1, -1, 1], uv: [1, 1], }, // 13 + { pos: [-1, 1, -1], uv: [0, 0], }, // 14 + { pos: [-1, 1, 1], uv: [1, 0], }, // 15 + // top + { pos: [ 1, 1, -1], uv: [0, 1], }, // 16 + { pos: [-1, 1, -1], uv: [1, 1], }, // 17 + { pos: [ 1, 1, 1], uv: [0, 0], }, // 18 + { pos: [-1, 1, 1], uv: [1, 0], }, // 19 + // bottom + { pos: [ 1, -1, 1], uv: [0, 1], }, // 20 + { pos: [-1, -1, 1], uv: [1, 1], }, // 21 + { pos: [ 1, -1, -1], uv: [0, 0], }, // 22 + { pos: [-1, -1, -1], uv: [1, 0], }, // 23 +``` + +Эти позиции и текстурные координаты +кладутся в буферы и подаются в вершинный шейдер +через атрибуты. + +Но обязательно ли делать именно так? А что если +мы хотим оставить только 8 углов +и 4 текстурные координаты? Например: + +```js +positions = [ + -1, -1, 1, // 0 + 1, -1, 1, // 1 + -1, 1, 1, // 2 + 1, 1, 1, // 3 + -1, -1, -1, // 4 + 1, -1, -1, // 5 + -1, 1, -1, // 6 + 1, 1, -1, // 7 +]; +uvs = [ + 0, 0, // 0 + 1, 0, // 1 + 0, 1, // 2 + 1, 1, // 3 +]; +``` + +А для каждой из 24 вершин мы бы указывали, какие из них использовать. + +```js +positionIndexUVIndex = [ + // front + 0, 1, // 0 + 1, 3, // 1 + 2, 0, // 2 + 3, 2, // 3 + // right + 1, 1, // 4 + 5, 3, // 5 + 3, 0, // 6 + 7, 2, // 7 + // back + 5, 1, // 8 + 4, 3, // 9 + 7, 0, // 10 + 6, 2, // 11 + // left + 4, 1, // 12 + 0, 3, // 13 + 6, 0, // 14 + 2, 2, // 15 + // top + 7, 1, // 16 + 6, 3, // 17 + 3, 0, // 18 + 2, 2, // 19 + // bottom + 1, 1, // 20 + 0, 3, // 21 + 5, 0, // 22 + 4, 2, // 23 +]; +``` + +Можно ли использовать это на GPU? Почему бы и нет!? + +Мы загрузим позиции и текстурные координаты +каждую в свою текстуру, как +рассматривалось в [статье о data-текстурах](webgl-data-textures.html). + +```js +function makeDataTexture(gl, data, numComponents) { + // расширяем данные до 4 значений на пиксель + const numElements = data.length / numComponents; + const expandedData = new Float32Array(numElements * 4); + for (let i = 0; i < numElements; ++i) { + const srcOff = i * numComponents; + const dstOff = i * 4; + for (let j = 0; j < numComponents; ++j) { + expandedData[dstOff + j] = data[srcOff + j]; + } + } + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip уровень + gl.RGBA32F, // формат + numElements, // ширина + 1, // высота + 0, // граница + gl.RGBA, // формат + gl.FLOAT, // тип + expandedData, + ); + // фильтрация не нужна + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + return tex; +} + +const positionTexture = makeDataTexture(gl, positions, 3); +const texcoordTexture = makeDataTexture(gl, uvs, 2); +``` + +Поскольку в текстуре может быть до 4 значений на пиксель, функция `makeDataTexture` +расширяет любые данные до 4 значений на пиксель. + +Далее создаём vertex array для хранения состояния атрибутов + +```js +// создаём vertex array object для хранения состояния атрибутов +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); +``` + +Теперь нужно загрузить индексы позиций и texcoord в буфер. + +```js +// Создаём буфер для индексов позиций и UV +const positionIndexUVIndexBuffer = gl.createBuffer(); +// Биндим его к ARRAY_BUFFER (думаем об этом как ARRAY_BUFFER = positionBuffer) +gl.bindBuffer(gl.ARRAY_BUFFER, positionIndexUVIndexBuffer); +// Кладём индексы позиций и texcoord в буфер +gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(positionIndexUVIndex), gl.STATIC_DRAW); +``` + +и настраиваем атрибут + +```js +// Включаем атрибут индекса позиции +gl.enableVertexAttribArray(posTexIndexLoc); + +// Говорим атрибуту индекса позиции/texcoord, как забирать данные из буфера \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md b/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md new file mode 100644 index 000000000..5909ea41a --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.md @@ -0,0 +1,46 @@ +Title: Доступ к текстурам по координатам пикселей в WebGL2 +Description: Доступ к текстурам по координатам пикселей в WebGL2 +TOC: Доступ к текстурам по координатам пикселей в WebGL2 + +## Вопрос: + +Из https://webgl2fundamentals.org/webgl/lessons/webgl-image-processing.html + +> WebGL2 добавляет возможность читать текстуру, используя координаты пикселей. Какой способ лучше - решать вам. Я считаю, что более распространено использование координат текстуры, чем координат пикселей. + +Нигде больше это не упоминается, кроме как передачи uniform с размерами текстуры в пикселях и вычисления оттуда, есть ли способ получить доступ к этим координатам пикселей без вычислений, как предполагается здесь? + + + +## Ответ: + +Вы можете читать отдельные пиксели/тексели из текстуры в WebGL2 с помощью `texelFetch`: + + vec4 color = texelFetch(someUniformSampler, ivec2(pixelX, pixelY), intMipLevel); + +Например, вычислить средний цвет текстуры, читая каждый пиксель: + +{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-1.html"}}} + +примечания: поскольку холст RGBA8 может получить только целочисленный результат. Можно изменить на какой-то формат с плавающей точкой, но это усложнит пример, который не о рендеринге, а о `texelFetch`. + +Конечно, просто изменив данные с R8 на RGBA8, мы можем делать 4 массива одновременно, если мы чередуем значения: + +{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-2.html"}}} + +Чтобы сделать больше, нужно придумать какой-то способ организовать данные и использовать вход в фрагментный шейдер, чтобы понять, где находятся данные. Например, мы снова чередуем данные, 5 массивов, так что данные идут 0,1,2,3,4,0,1,2,3,4,0,1,2,3,4. + +Давайте вернемся к R8 и сделаем 5 отдельных массивов. Нам нужно нарисовать 5 пикселей. Мы можем сказать, какой пиксель рисуется, посмотрев на `gl_FragCoord`. Мы можем использовать это для смещения, какие пиксели мы смотрим, и передать, сколько пропустить. + +{{{example url="../webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2-example-3.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + bogersja + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md b/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md new file mode 100644 index 000000000..d9a40039a --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-apply-a-displacement-map-and-specular-map.md @@ -0,0 +1,326 @@ +Title: Применение карты смещения и карты бликов +Description: Применение карты смещения и карты бликов +TOC: Применение карты смещения и карты бликов + +## Вопрос: + +Я пытаюсь применить как карту смещения, так и карту бликов для Земли, и только карту смещения для Луны. + +Я мог бы преобразовать карту высот в карту нормалей, но если я использую ту же карту высот для применения карты смещения, это не работает так, как я ожидал. + +Вот пример изображения: + +[![Пример 1][1]][1] + +как вы можете видеть, неровности вокруг Земли и Луны, но нет реальных различий в высоте. + +Если я применяю карту бликов к Земле, Земля становится такой: + +[![Пример 2][2]][2] + +Я хочу, чтобы только океан Земли блестел, но мой код превращает Землю в полностью черную, я вижу только некоторые белые точки на Земле... + +Эти текстуры взяты с этого [сайта][3] + +Вот мой код вершинного шейдера и фрагментного шейдера: + + "use strict"; + const loc_aPosition = 3; + const loc_aNormal = 5; + const loc_aTexture = 7; + const VSHADER_SOURCE = + `#version 300 es + layout(location=${loc_aPosition}) in vec4 aPosition; + layout(location=${loc_aNormal}) in vec4 aNormal; + layout(location=${loc_aTexture}) in vec2 aTexCoord; + + + uniform mat4 uMvpMatrix; + uniform mat4 uModelMatrix; // Матрица модели + uniform mat4 uNormalMatrix; // Матрица преобразования нормали + + uniform sampler2D earth_disp; + uniform sampler2D moon_disp; + + //uniform float earth_dispScale; + //uniform float moon_dispScale; + + //uniform float earth_dispBias; + //uniform float moon_dispBias; + + uniform bool uEarth; + uniform bool uMoon; + + + out vec2 vTexCoord; + out vec3 vNormal; + out vec3 vPosition; + + + void main() + { + + float disp; + + if(uEarth) + disp = texture(earth_disp, aTexCoord).r; //Извлечение цветовой информации из изображения + else if(uMoon) + disp = texture(moon_disp, aTexCoord).r; //Извлечение цветовой информации из изображения + + vec4 displace = aPosition; + + float displaceFactor = 2.0; + float displaceBias = 0.5; + + if(uEarth || uMoon) //Использование карты смещения + { + displace += (displaceFactor * disp - displaceBias) * aNormal; + gl_Position = uMvpMatrix * displace; + } + else //Не используем карту смещения + gl_Position = uMvpMatrix * aPosition; + + // Вычисляем позицию вершины в мировой системе координат + vPosition = vec3(uModelMatrix * aPosition); + + vNormal = normalize(vec3(uNormalMatrix * aNormal)); + vTexCoord = aTexCoord; + + }`; + + // Программа фрагментного шейдера + const FSHADER_SOURCE = + `#version 300 es + precision mediump float; + + uniform vec3 uLightColor; // Цвет света + uniform vec3 uLightPosition; // Позиция источника света + uniform vec3 uAmbientLight; // Цвет окружающего света + + uniform sampler2D sun_color; + uniform sampler2D earth_color; + uniform sampler2D moon_color; + + uniform sampler2D earth_bump; + uniform sampler2D moon_bump; + + uniform sampler2D specularMap; + + + in vec3 vNormal; + in vec3 vPosition; + in vec2 vTexCoord; + out vec4 fColor; + + uniform bool uIsSun; + uniform bool uIsEarth; + uniform bool uIsMoon; + + + + vec2 dHdxy_fwd(sampler2D bumpMap, vec2 UV, float bumpScale) + { + vec2 dSTdx = dFdx( UV ); + vec2 dSTdy = dFdy( UV ); + float Hll = bumpScale * texture( bumpMap, UV ).x; + float dBx = bumpScale * texture( bumpMap, UV + dSTdx ).x - Hll; + float dBy = bumpScale * texture( bumpMap, UV + dSTdy ).x - Hll; + return vec2( dBx, dBy ); + } + + vec3 pertubNormalArb(vec3 surf_pos, vec3 surf_norm, vec2 dHdxy) + { + vec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) ); + vec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) ); + vec3 vN = surf_norm; // нормализованная + vec3 R1 = cross( vSigmaY, vN ); + vec3 R2 = cross( vN, vSigmaX ); + float fDet = dot( vSigmaX, R1 ); + fDet *= ( float( gl_FrontFacing ) * 2.0 - 1.0 ); + vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 ); + return normalize( abs( fDet ) * surf_norm - vGrad ); + } + + + + void main() + { + vec2 dHdxy; + vec3 bumpNormal; + float bumpness = 1.0; + if(uIsSun) + fColor = texture(sun_color, vTexCoord); + else if(uIsEarth) + { + fColor = texture(earth_color, vTexCoord); + dHdxy = dHdxy_fwd(earth_bump, vTexCoord, bumpness); + } + else if(uIsMoon) + { + fColor = texture(moon_color, vTexCoord); + dHdxy = dHdxy_fwd(moon_bump, vTexCoord, bumpness); + } + + + + // Нормализуем нормаль, потому что она интерполируется и больше не имеет длину 1.0 + vec3 normal = normalize(vNormal); + + + // Вычисляем направление света и делаем его длину равной 1. + vec3 lightDirection = normalize(uLightPosition - vPosition); + + + + // Скалярное произведение направления света и ориентации поверхности (нормали) + float nDotL; + if(uIsSun) + nDotL = 1.0; + else + nDotL = max(dot(lightDirection, normal), 0.0); + + + + // Вычисляем финальный цвет из диффузного отражения и окружающего отражения + vec3 diffuse = uLightColor * fColor.rgb * nDotL; + vec3 ambient = uAmbientLight * fColor.rgb; + float specularFactor = texture(specularMap, vTexCoord).r; //Извлечение цветовой информации из изображения + + + + + vec3 diffuseBump; + if(uIsEarth || uIsMoon) + { + bumpNormal = pertubNormalArb(vPosition, normal, dHdxy); + diffuseBump = min(diffuse + dot(bumpNormal, lightDirection), 1.1); + } + + vec3 specular = vec3(0.0); + float shiness = 12.0; + vec3 lightSpecular = vec3(1.0); + + if(uIsEarth && nDotL > 0.0) + { + vec3 v = normalize(-vPosition); // Позиция глаза + vec3 r = reflect(-lightDirection, bumpNormal); // Отражение от поверхности + specular = lightSpecular * specularFactor * pow(dot(r, v), shiness); + } + + //Обновляем финальный цвет + if(uIsEarth) + fColor = vec4( (diffuse * diffuseBump * specular) + ambient, fColor.a); // Блики + else if(uIsMoon) + fColor = vec4( (diffuse * diffuseBump) + ambient, fColor.a); + else if(uIsSun) + fColor = vec4(diffuse + ambient, fColor.a); + }`; + + +Можете ли вы сказать мне, где мне нужно проверить? + + [1]: https://i.stack.imgur.com/eJgLg.png + [2]: https://i.stack.imgur.com/zRSZu.png + [3]: http://planetpixelemporium.com/earth.html + +## Ответ: + +Если бы это был я, я бы сначала упростил шейдер до самой простой вещи и посмотрел, получаю ли я то, что хочу. Вы хотите блики, так получаете ли вы блики только с расчетами бликов в ваших шейдерах? + +Обрезка ваших шейдеров до простого рисования плоского освещения по Фонгу не дала правильных результатов. + +Эта строка: + +``` +fColor = vec4( (diffuse * specular) + ambient, fColor.a); +``` + +должна была быть: + +``` +fColor = vec4( (diffuse + specular) + ambient, fColor.a); +``` + +Вы добавляете блики, а не умножаете на них. + +{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-1.html"}}} + +Теперь мы можем добавить карту бликов: + +{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-2.html"}}} + +Затем вам, возможно, не стоит использовать много булевых условий в вашем шейдере. Либо создайте разные шейдеры, либо найдите способ сделать это без булевых значений. Так, например, нам не нужны: + +``` +uniform sampler2D earth_disp; +uniform sampler2D moon_disp; + +uniform sampler2D sun_color; +uniform sampler2D earth_color; +uniform sampler2D moon_color; + +uniform sampler2D earth_bump; +uniform sampler2D moon_bump; + +uniform bool uIsSun; +uniform bool uIsEarth; +uniform bool uIsMoon; +``` + +мы можем просто иметь: + +``` +uniform sampler2D displacementMap; +uniform sampler2D surfaceColor; +uniform sampler2D bumpMap; +``` + +Затем мы можем установить `displacementMap` и `bumpMap` на текстуру одного пикселя 0,0,0,0, и не будет ни смещения, ни неровностей. + +Что касается разного освещения для солнца, учитывая, что солнце не использует ни карту неровностей, ни карту смещения, ни даже освещение вообще, возможно, было бы лучше использовать другой шейдер, но мы также можем просто добавить значение `maxDot` так: + +``` +uniform float maxDot; + +... + + nDotL = max(dot(lightDirection, normal), maxDot) +``` + +Если `maxDot` равен нулю, мы получим нормальное скалярное произведение. Если `maxDot` равен единице, мы не получим освещения. + +{{{example url="../webgl-qna-apply-a-displacement-map-and-specular-map-example-3.html"}}} + +Что касается смещения, смещение работает только на вершинах, поэтому вам нужно много вершин в вашей сфере, чтобы увидеть любое смещение. + +Также была ошибка, связанная со смещением. Вы передаете нормали как vec4, и эта строка: + + displace += (displaceFactor * disp - displaceBias) * aNormal; + +В итоге добавляет смещение vec4. Другими словами, допустим, вы начали с `a_Position` равным `vec4(1,0,0,1)`, что было бы на левой стороне сферы. `aNormal`, потому что вы объявили его как `vec4`, вероятно, тоже `vec4(1,0,0,1)`. Предполагая, что вы фактически передаете данные vec3 нормали через атрибуты из вашего буфера, значение по умолчанию для W равно 1. Допустим, `disp` равен 1, `displaceFactor` равен 2, а `displaceBias` равен 0.5, что у вас было. Вы получаете: + + displace = vec4(1,0,0,1) + (2 * 1 + 0.5) * vec4(1,0,0,1) + displace = vec4(1,0,0,1) + (1.5) * vec4(1,0,0,1) + displace = vec4(1,0,0,1) + vec4(1.5,0,0,1.5) + displace = vec4(2.5,0,0,2.5) + +Но вы не хотите, чтобы W был 2.5. Одно исправление - просто использовать xyz часть нормали: + + displace.xyz += (displaceFactor * disp - displaceBias) * aNormal.xyz; + +Более нормальное исправление - объявить атрибут нормали только как vec3: + + in vec3 aNormal; + + displace.xyz += (displaceFactor * disp - displaceBias) * aNormal; + +В моем примере выше сферы имеют только радиус = 1, поэтому мы хотим только немного скорректировать это смещение. Я установил `displaceFactor` равным 0.1 и `displaceBias` равным 0. + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + ZeroFive005 + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md b/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md new file mode 100644 index 000000000..804af4c88 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.md @@ -0,0 +1,154 @@ +Title: Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер? +Description: Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер? +TOC: Can anyone explain what this GLSL fragment shader is doing? + +## Вопрос: + +Я понимаю, что это вопрос, ориентированный на математику, но... если вы посмотрите на [эту веб-страницу](https://threejs.org/examples/?q=shader#webgl_shader). (и у вас есть хорошая видеокарта) + +Если вы посмотрите на исходный код, вы заметите страшно выглядящий фрагментный шейдер. + +Я не ищу подробного объяснения, но идею о том, что происходит, или источник информации о том, что именно происходит здесь.. Я не ищу руководство по GLSL, но информацию о математике. Я понимаю, что это может быть лучше подходит для сайта Math StackExchange, но подумал, что попробую здесь сначала... + + + +## Ответ: + +[Monjori](http://www.pouet.net/prod.php?which=52761) из демо-сцены. + +Простой ответ - он использует формулу для генерации паттерна. WebGL будет вызывать эту функцию один раз для каждого пикселя на экране. Единственные вещи, которые будут меняться, это time и gl_FragCoord, который является местоположением пикселя, который рисуется. + +Давайте разберем это немного + + + // это разрешение окна + uniform vec2 resolution; + + // это счетчик в секундах. + uniform float time; + + void main() { + // gl_FragCoord - это позиция пикселя, который рисуется + // поэтому этот код делает p значением, которое идет от -1 до +1 + // x и y + vec2 p = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy; + + // a = время ускоренное в 40 раз + float a = time*40.0; + + // объявляем кучу переменных. + float d,e,f,g=1.0/40.0,h,i,r,q; + + // e идет от 0 до 400 по экрану + e=400.0*(p.x*0.5+0.5); + + // f идет от 0 до 400 вниз по экрану + f=400.0*(p.y*0.5+0.5); + + // i идет от 200 + или - 20 на основе + // sin от e * 1/40 + замедленное время / 150 + // или другими словами замедлить еще больше. + // e * 1/40 означает e идет от 0 до 1 + i=200.0+sin(e*g+a/150.0)*20.0; + + // d это 200 + или - 18.0 + или - 7 + // первый +/- это cos от 0.0 до 0.5 вниз по экрану + // второй +/- это cos от 0.0 до 1.0 по экрану + d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; + + // Я останавливаюсь здесь. Вы, вероятно, можете разобрать остальное + // смотрите ответ + r=sqrt(pow(i-e,2.0)+pow(d-f,2.0)); + q=f/r; + e=(r*cos(q))-a/2.0;f=(r*sin(q))-a/2.0; + d=sin(e*g)*176.0+sin(e*g)*164.0+r; + h=((f+d)+a/2.0)*g; + i=cos(h+r*p.x/1.3)*(e+e+a)+cos(q*g*6.0)*(r+h/3.0); + h=sin(f*g)*144.0-sin(e*g)*212.0*p.x; + h=(h+(f-e)*q+sin(r-(a+h)/7.0)*10.0+i/4.0)*g; + i+=cos(h*2.3*sin(a/350.0-q))*184.0*sin(q-(r*4.3+a/12.0)*g)+tan(r*g+h)*184.0*cos(r*g+h); + i=mod(i/5.6,256.0)/64.0; + if(i<0.0) i+=4.0; + if(i>=2.0) i=4.0-i; + d=r/350.0; + d+=sin(d*d*8.0)*0.52; + f=(sin(a*g)+1.0)/2.0; + gl_FragColor=vec4(vec3(f*i/1.6,i/2.0+d/13.0,i)*d*p.x+vec3(i/1.3+d/8.0,i/2.0+d/18.0,i)*d*(1.0-p.x),1.0); + } + +Одна из вещей, которую хорошо попробовать, чтобы увидеть, что происходит, это вставить ранние выходы в шейдер. Сначала вы можете увидеть шейдер здесь + +http://glsl.heroku.com/e#1579.0 + +или + +https://www.shadertoy.com/view/lsfyRS + +Если мы перейдем к строке 11 + + e=400.0*(p.x*0.5+0.5); + +и вставим сразу после нее что-то вроде этого + + e=400.0*(p.x*0.5+0.5); + gl_FragColor = vec4(e / 400.0, 0, 0, 1); + return; + +Пока мы конвертируем значение в что-то от 0 до 1, мы можем увидеть результат + +например, спускаясь к строке 14 + + d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; + +Поскольку мы знаем, что это идет от 200 +/- 18 +/- 7, это 175 + 225, поэтому конвертируем это в 0 до 1 с + + d=200.0+cos(f*g/2.0)*18.0+cos(e*g)*7.0; + float tmp = (d - 175.0) / 50.0; + gl_FragColor = vec4(tmp, 0, 0, 1); + return; + +даст вам некоторое представление о том, что он делает. + +Я уверен, что вы можете разобрать остальное. + +
+
Вопрос и процитированные части являются + CC BY-SA 3.0 от + Alex + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md b/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md new file mode 100644 index 000000000..aa70eafdc --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-creating-a-smudge-liquify-effect.md @@ -0,0 +1,73 @@ +Title: Создание эффекта смазывания/жидкости (smudge/liquify) +Description: Создание эффекта смазывания/жидкости (smudge/liquify) +TOC: Создание эффекта смазывания/жидкости (smudge/liquify) + +## Вопрос: + +Я пытаюсь найти информацию или примеры, которые можно использовать для создания эффекта смазывания/жидкости, который постепенно анимируется обратно к исходному состоянию. + +Сначала я рассматривал вариант с использованием three.js или pixi.js для рендеринга текста, а затем с помощью событий мыши и ray casting вытягивать меш из позиции. Ближе всего, что я нашёл — это: + +https://codepen.io/shshaw/pen/qqVgbg + + let renderer = PIXI.autoDetectRenderer(window.innerWidth, + window.innerHeight, { transparent: true }); + +Думаю, в идеале я бы рендерил текст как изображение, а затем применял бы эффект смазывания к пикселям, которые медленно возвращались бы к исходному состоянию. Похоже на это: + +http://www.duhaihang.com/#/work/ + +Думаю, мне нужен кастомный GLSL-шейдер и какой-то буфер для хранения исходного и текущего состояния пикселей изображения. + +Любая помощь или направление будет очень полезно. + +## Ответ: + +Оба варианта довольно прямолинейны. + +Первый, как вы и сказали, — делаете меш (сетку) вершин, рисующую плоскость. Накладываете текстуру на плоскость, при перемещении мыши добавляете смещение каждой вершине, которую касается мышь. Со временем сбрасываете смещение обратно к 0 (т.е. 0 смещения). + +Вот пример: здесь смещается только одна вершина на случайную величину, а не что-то более предсказуемое. В конце я просто сохраняю время, к которому смещение должно исчезнуть, затем в шейдере делаю простой линейный lerp (можно сделать более сложный для эффекта bounce и т.д.). Так почти всё происходит в шейдере. + +{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-1.html"}}} + +Во втором варианте вместо смещения вершин делаете текстуру смещений, со временем сбрасываете это смещение к 0. + +Пример затухания можно посмотреть [здесь](https://stackoverflow.com/a/38407507/128511). Если взять этот пример и вместо рисования случайного квадрата рисовать под мышью, а затем использовать эту текстуру как смещение для основного изображения. Под смещением я имею в виду, что обычно вы берёте текстуру во фрагментном шейдере так: + + vec4 color = texture2D(someTexture, someTextureCoords); + +Вместо этого вы хотите сместить координаты текстуры с помощью displacement, примерно так: + + // если текстура смещений того же размера, что и основная, можно использовать те же координаты + + // сначала получаем смещение и переводим в диапазон -1 <-> 1 + // используем только R и G каналы, которые станут U и V смещениями для координат текстуры + vec2 displacement = texture2D(displacementTexture, someTextureCoords).rg * 2. - 1.; + + vec2 uv = someTextureCoords + displacement * displacementRange; + vec4 color = texture2d(someTexture, uv); + +Вот пример выше, используемый для смещения: + +{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-2.html"}}} + +Осталось только рисовать под мышью, а не в случайном месте: + +{{{example url="../webgl-qna-creating-a-smudge-liquify-effect-example-3.html"}}} + +Точный эффект из вашего второго примера, похоже, использует какое-то шумовое преобразование displacement. Можно воспользоваться [WebGL Inspector](https://benvanik.github.io/WebGL-Inspector/) или [Shader Editor](https://github.com/spite/ShaderEditorExtension), чтобы посмотреть, что происходит в шейдерах. + +[Вот ещё пример](https://codepen.io/greggman/pen/bgXgvr), который создаёт текстуру смещений, смещающую больше к центру, чем к краю. + +Пояснение: я не смотрел детали того, как работают ваши примеры, я лишь предполагаю, что они делают что-то *похожее*. Лучший способ узнать, что они реально делают — посмотреть их код и воспользоваться инструментами выше, чтобы заглянуть внутрь. Может быть, они используют не прямое смещение, а, например, нормали как displacement. Может быть, вместо рисования сплошным цветом (2-й и 3-й примеры) или текстурой (4-й пример), они используют процедурный паттерн или экранные координаты для повторяющейся текстуры. Может быть, текстура смещений — отдельная текстура, а есть ещё "mix mask", которую рисуют белым и затухают в чёрный, чтобы определить, сколько displacement применять. В WebGL бесконечно много способов реализовать подобные эффекты. + + + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + plexus + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md b/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md new file mode 100644 index 000000000..ce95d9d26 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.md @@ -0,0 +1,38 @@ +Title: FPS-подобное движение камеры с базовыми матричными трансформациями +Description: FPS-подобное движение камеры с базовыми матричными трансформациями +TOC: FPS-подобное движение камеры с базовыми матричными трансформациями + +## Вопрос: + +У меня простая сцена в WebGL, где я храню каждую трансформацию (для камеры и моделей) в одной модели/видовой матрице и устанавливаю их, вращая и перемещая эту матрицу. + +Я хочу иметь возможность вращать камеру вокруг и когда я "двигаюсь вперёд", двигаться в направлении, куда указывает камера. + +Пока что я изменил [этот][1] код на это: + + mat4.identity(mvMatrix); + mat4.rotateX(mvMatrix, degToRad(elev), mvMatrix); + mat4.rotateY(mvMatrix, degToRad(ang), mvMatrix); + mat4.rotateZ(mvMatrix, degToRad(-roll), mvMatrix); + mat4.translate(mvMatrix, [-px, -py, -pz], mvMatrix); +поскольку это не работало как было, и это вроде работает, пока вы не делаете экстремальное вращение (больше 90 градусов). + +Это не критично для того, что я делаю, но я хочу знать. Это лучшее, что я могу получить, не отходя от расчёта ориентации камеры таким образом? + + [1]: https://stackoverflow.com/questions/18463868/webgl-translation-after-rotation-of-the-camera-as-an-fps + +## Ответ: + +WebGL камеры обычно указывают по оси -Z, так что чтобы двигаться в направлении, куда смотрит камера, вы просто добавляете Z-ось камеры (элементы 8, 9, 10) к позиции камеры, умноженной на некоторую скорость. + +{{{example url="../webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations-example-1.html"}}} + + + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + George Daskalakis + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md b/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md new file mode 100644 index 000000000..e06cea7de --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-get-audio-data-into-a-shader.md @@ -0,0 +1,45 @@ +Title: Как получить аудио данные в шейдер +Description: Как получить аудио данные в шейдер +TOC: Как получить аудио данные в шейдер + +## Вопрос: + +Как я могу добавить поддержку аудио визуализации к этому классу, я хотел бы добавить объект Audio() как вход в GLSL фрагментный шейдер. Пример этого https://www.shadertoy.com/view/Mlj3WV. Я знаю, что такие вещи можно делать в Canvas 2d с анализом формы волны, но этот метод opengl намного более плавный. + + +``` +/* Код из https://raw.githubusercontent.com/webciter/GLSLFragmentShader/1.0.0/GLSLFragmentShader.js */ + +/* функция рендеринга */ + +/* установить uniform переменные для шейдера */ + gl.uniform1f( currentProgram.uniformsCache[ 'time' ], parameters.time / 1000 ); +gl.uniform2f( currentProgram.uniformsCache[ 'mouse' ], parameters.mouseX, parameters.mouseY ); + +/* я хотел бы что-то вроде этого */ +gl.uniform2f( currentProgram.uniformsCache[ 'fft' ], waveformData ); + + +``` + +Шейдер в примере ShaderToy принимает float как fft, но это просто обновляет всю строку полосок, а не отдельные значения полосок. Я хотел бы манипуляции в реальном времени всех полосок. + +Я искал в MDN, но не понимаю, как это включить, я также смотрел исходный код shadertoy.com, но не могу понять, как они достигли этого. + +## Ответ: + +ShaderToy не предоставляет FFT как float. Он предоставляет данные FFT как текстуру + + +{{{example url="../webgl-qna-how-to-get-audio-data-into-a-shader-example-1.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + David Clews + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md b/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md new file mode 100644 index 000000000..200d862b9 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.md @@ -0,0 +1,83 @@ +Title: Как получить автодополнение кода для WebGL в Visual Studio Code +Description: Как получить автодополнение кода для WebGL в Visual Studio Code +TOC: Как получить автодополнение кода для WebGL в Visual Studio Code + +## Вопрос: + +У меня есть школьный проект, и мне нужно использовать WEBGL. Но довольно сложно писать весь код без автодополнения. Я не нашел подходящего расширения. У вас есть идеи? + +## Ответ: + +Для того чтобы Visual Studio Code давал вам автодополнение, ему нужно знать типы переменных. + +Так, например, если у вас есть это + +``` +const gl = init(); +``` + +VSCode не имеет представления о том, какой тип у переменной `gl`, поэтому он не может автодополнять. Но вы можете сказать ему тип, добавив JSDOC стиль комментария выше, как это + +``` +/** @type {WebGLRenderingContext} */ +const gl = init(); +``` + +Теперь он будет автодополнять + +[![enter image description here][1]][1] + + +То же самое верно для HTML элементов. Если вы делаете это + +``` +const canvas = document.querySelector('#mycanvas'); +``` + +VSCode не имеет представления о том, какой это тип элемента, но вы можете сказать ему + +``` +/** @type {HTMLCanvasElement} */ +const canvas = document.querySelector('#mycanvas'); +``` + +Теперь он будет знать, что это `HTMLCanvasElement` + +[![enter image description here][2]][2] + +И, поскольку он знает, что это `HTMLCanvasElement`, он знает, что `.getContext('webgl')` возвращает `WebGLRenderingContext`, поэтому он автоматически предложит автодополнение для контекста тоже + +[![enter image description here][3]][3] + +Обратите внимание, что если вы передаете canvas в какую-то функцию, то снова VSCode не имеет представления о том, что возвращает эта функция. Другими словами + +``` +/** @type {HTMLCanvasElement} */ +const canvas = document.querySelector('#mycanvas'); +const gl = someLibraryInitWebGL(canvas); +``` + +Вы больше не получите автодополнение, поскольку VSCode не имеет представления о том, что возвращает `someLibraryInitWebGL`, поэтому следуйте правилу сверху и скажите ему. + +``` +/** @type {HTMLCanvasElement} */ +const canvas = document.querySelector('#mycanvas'); + +/** @type {WebGLRenderingContext} */ +const gl = someLibraryInitWebGL(canvas); +``` + +Вы можете увидеть другие JSDOC аннотации [здесь](https://jsdoc.app/), если хотите документировать свои собственные функции, например, их аргументы и типы возврата. + + [1]: https://i.stack.imgur.com/8mvFM.png + [2]: https://i.stack.imgur.com/oArWf.png + [3]: https://i.stack.imgur.com/7zR4q.png + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + Nikola Kovač + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md b/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md new file mode 100644 index 000000000..19663808e --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-get-pixelize-effect-in-webgl-.md @@ -0,0 +1,55 @@ +Title: Как получить эффект пикселизации в WebGL? +Description: Как получить эффект пикселизации в WebGL? +TOC: Как получить эффект пикселизации в WebGL? + +## Вопрос: + +Я хочу симулировать эффект старого ПК с низким разрешением, как Atari или Commodore, в WebGL. Есть ли способ нарисовать изображение, а затем как-то сделать пиксели больше? + +Я новичок в WebGL, так как мне начать делать этот эффект? + +Я нашёл [это](https://threejs.org/examples/#webgl_postprocessing_nodes), там есть эффект мозаики, но он использует three.js, а я хочу сделать это без фреймворков. + +## Ответ: + +Есть много способов сделать это. Самый простой — просто рендерить в низкоразрешающую текстуру, прикрепив её к framebuffer, а затем рендерить эту текстуру на canvas с фильтрацией текстуры, установленной на `NEAREST`. + +Вот пример. Он использует [TWGL](http://twgljs.org), который не является фреймворком, просто помощник, чтобы сделать WebGL менее многословным. Смотрите комментарии (и [документацию](http://twgljs.org/docs/)), если хотите перевести это в многословный сырой WebGL. + +Если вы новичок в WebGL, [я бы предложил начать отсюда](http://webglfundamentals.org): + +{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-1.html"}}} + +Также распространено рендерить в текстуру (как выше), но текстуру более высокого разрешения, а затем фильтровать её вниз, используя шейдеры, мипы и/или линейную фильтрацию. Преимущество в том, что вы получите больше сглаживания: + +{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-2.html"}}} + +--- + +# обновление + +В 2020 году, возможно, самое простое, что вы можете сделать — просто сделать canvas с разрешением, которое вы хотите, например 32x32, и установить его CSS размер больше, а затем использовать настройку CSS `image-rendering: pixelated`, чтобы сказать браузеру не сглаживать его при масштабировании изображения: + +``` + +``` + +{{{example url="../webgl-qna-how-to-get-pixelize-effect-in-webgl--example-3.html"}}} + + + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + Maciej Kozieja + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md new file mode 100644 index 000000000..9f1b8e5b8 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.md @@ -0,0 +1,142 @@ +Title: Как реализовать зум от мыши в 2D WebGL +Description: Как реализовать зум от мыши в 2D WebGL +TOC: Как реализовать зум от мыши в 2D WebGL + +## Вопрос: + +Я сейчас делаю 2D-рисовалку на WebGL. Хочу реализовать зум к точке под курсором мыши, как в [этом примере](https://stackoverflow.com/a/53193433/2594567). Но не могу понять, как применить решение из того ответа в своём случае. + +Я сделал базовый зум через масштабирование матрицы камеры. Но он увеличивает к левому верхнему углу canvas, потому что это начало координат (0,0), заданное проекцией (насколько я понимаю). + +Базовый пан и зум реализованы: +![img](https://i.imgur.com/asRTm1e.gif) + +### Моя функция draw (включая вычисления матриц) выглядит так: + +```javascript +var projection = null; +var view = null; +var viewProjection = null; + +function draw(gl, camera, sceneTree){ + // projection matrix + projection = new Float32Array(9); + mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight); + + // camera matrix + view = new Float32Array(9); + mat3.fromTranslation(view, camera.translation); + mat3.rotate(view, view, toRadians(camera.rotation)); + mat3.scale(view, view, camera.scale); + // view matrix + mat3.invert(view, view) + + // VP matrix + viewProjection = new Float32Array(9); + mat3.multiply(viewProjection, projection, view); + + // go through scene tree: + // - build final matrix for each object + // e.g: u_matrix = VP x Model (translate x rotate x scale) + + // draw each object in scene tree + // ... +} +``` + +### Вершинный шейдер: +``` +attribute vec2 a_position; + +uniform mat3 u_matrix; + +void main() { + gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); +} +``` + +### Функция зума: + +```javascript + +function screenToWorld(screenPos){ + // normalized screen position + let nsp = [ + 2.0 * screenPos[0] / this.gl.canvas.width - 1, + - 2.0 * screenPos[1] / this.gl.canvas.height + 1 + ]; + + let inverseVP = new Float32Array(9); + mat3.invert(inverseVP, viewProjection); + + let worldPos = [0, 0]; + return vec2.transformMat3(worldPos, nsp, inverseVP); +} + +var zoomRange = [0.01, 2]; + +canvas.addEventListener('wheel', (e) => { + let oldZoom = camera.scale[0]; + let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]); + + camera.scale = [zoom, zoom]; + + let zoomPoint = screenToWorld([e.clientX, e.clientY]); + // totally breaks if enable this line + //vec2.copy(camera.translation, zoomPoint); + + // call draw function again + draw(); + +}, false); +``` + + +Если я применяю `zoomPoint` к трансляции камеры, значения `zoomPoint` (и позиция камеры соответственно) начинают неуправляемо расти при каждом событии зума (неважно, увеличиваю я или уменьшаю), и объекты в сцене сразу выходят из вида. + +Буду очень благодарен за любые идеи или подсказки, что я делаю не так. Спасибо. + +## Ответ: + +Поскольку вы не выложили **минимальный воспроизводимый пример** в самом вопросе, я не мог проверить с вашей библиотекой матриц. Используя свою, я смог реализовать зум так: + +``` + const [clipX, clipY] = getClipSpaceMousePosition(e); + + // позиция до зума + const [preZoomX, preZoomY] = m3.transformPoint( + m3.inverse(viewProjectionMat), + [clipX, clipY]); + + // умножаем движение колеса на текущий уровень зума + // чтобы зумить меньше при большом увеличении и больше при малом + const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01); + camera.zoom = Math.max(0.02, Math.min(100, newZoom)); + + updateViewProjection(); + + // позиция после зума + const [postZoomX, postZoomY] = m3.transformPoint( + m3.inverse(viewProjectionMat), + [clipX, clipY]); + + // камеру нужно сдвинуть на разницу до и после + camera.x += preZoomX - postZoomX; + camera.y += preZoomY - postZoomY; +``` + +Обратите внимание, что зум — это противоположность scale. Если zoom = 2, то я хочу, чтобы всё выглядело в 2 раза больше. Для этого нужно *уменьшить* пространство камеры, то есть масштабировать его на 1 / zoom. + +Пример: + +{{{example url="../webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl-example-1.html"}}} + +Обратите внимание, что я добавил camera.rotation, чтобы убедиться, что всё работает и при повороте. Похоже, работает. [Вот пример с зумом, панорамой и вращением](https://jsfiddle.net/greggman/mdpxw3n6/) + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + nicktgn + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md new file mode 100644 index 000000000..8a5b80e9c --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-import-a-heightmap-in-webgl.md @@ -0,0 +1,211 @@ +Title: Как импортировать карту высот в WebGL +Description: Как импортировать карту высот в WebGL +TOC: Как импортировать карту высот в WebGL + +## Вопрос: + +Я знаю, что в теории нужно сначала найти координаты на карте высот, например (x = `ширина HM / ширина Terrain * x Terrain`) и y координату (`y = высота HM / высота Terrain * y Terrain`), и после получения местоположения на карте высот мы получаем реальную высоту по формуле `min_height + (colorVal / (max_color - min_color) * *max_height - min_height`), возвращая Z значение для конкретного сегмента. + +Но как я могу реально импортировать карту высот и получить её параметры? Я пишу на JavaScript без дополнительных библиотек (three, babylon). + +**edit** + +Сейчас я жёстко кодирую Z значения на основе диапазонов x и y: + + Plane.prototype.modifyGeometry=function(x,y){ + if((x>=0&&x<100)&&(y>=0&&y<100)){ + return 25; + } + else if((x>=100&&x<150)&&(y>=100&&y<150)){ + return 20; + } + else if((x>=150&&x<200)&&(y>=150&&y<200)){ + return 15; + } + else if((x>=200&&x<250)&&(y>=200&&y<250)){ + return 10; + } + else if((x>=250&&x<300)&&(y>=250&&y<300)){ + return 5; + } + else{ + return 0; + } + +** edit ** + +Я могу получить плоскую сетку (или с случайно сгенерированными высотами), но как только я добавляю данные изображения, получаю пустой экран (хотя ошибок нет). Вот код (я немного изменил его): + + + + var gl; + var canvas; + + var img = new Image(); + // img.onload = run; + img.crossOrigin = 'anonymous'; + img.src = 'https://threejsfundamentals.org/threejs/resources/images/heightmap-96x64.png'; + + + var gridWidth; + var gridDepth; + var gridPoints = []; + var gridIndices = []; + var rowOff = 0; + var rowStride = gridWidth + 1; + var numVertices = (gridWidth * 2 * (gridDepth + 1)) + (gridDepth * 2 * (gridWidth + 1)); + + + //создание плоскости + function generateHeightPoints() { + + var ctx = document.createElement("canvas").getContext("2d"); //используем 2d canvas для чтения изображения + ctx.canvas.width = img.width; + ctx.canvas.height = img.height; + ctx.drawImage(img, 0, 0); + var imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); + + gridWidth = imgData.width - 1; + gridDepth = imgData.height - 1; + + for (var z = 0; z <= gridDepth; ++z) { + for (var x = 0; x <= gridWidth; ++x) { + var offset = (z * imgData.width + x) * 4; + var height = imgData.data[offset] * 10 / 255; + gridPoints.push(x, height, z); + } + } + } + + function generateIndices() { + for (var z = 0; z<=gridDepth; ++z) { + rowOff = z*rowStride; + for(var x = 0; x + +Читайте его, загружая изображение, рисуя на 2D canvas, вызывая getImageData. Затем просто читайте красные значения для высоты. + +{{{example url="../webgl-qna-how-to-import-a-heightmap-in-webgl-example-2.html"}}} + +Затем вместо создания сетки линий создайте сетку треугольников. Есть много способов это сделать. Вы можете поставить 2 треугольника на квадрат сетки. Этот код ставит 4. Вам также нужно генерировать нормали. Я скопировал код для генерации нормалей из [этой статьи](https://webglfundamentals.org/webgl/lessons/webgl-3d-geometry-lathe.html), который является довольно универсальным кодом генерации нормалей. Будучи сеткой, вы можете создать генератор нормалей, специфичный для сетки, который будет быстрее, поскольку будучи сеткой вы знаете, какие вершины общие. + +Этот код также использует [twgl](https://twgljs.org), потому что WebGL слишком многословен, но должно быть понятно, как сделать это в обычном WebGL, читая названия функций twgl. + +{{{example url="../webgl-qna-how-to-import-a-heightmap-in-webgl-example-3.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + cosmo + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md b/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md new file mode 100644 index 000000000..d8c9e3a5c --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-load-images-in-the-background-with-no-jank.md @@ -0,0 +1,70 @@ +Title: Как загружать изображения в фоне без задержек +Description: Как загружать изображения в фоне без задержек +TOC: Как загружать изображения в фоне без задержек + +## Вопрос: + +В нашем WebGL приложении я пытаюсь загружать и декодировать текстуры в web worker, чтобы избежать задержек рендеринга в основном потоке. Использование createImageBitmap в worker и передача image bitmap обратно в основной поток работает хорошо, но в Chrome это использует **три** или больше (возможно, в зависимости от количества ядер?) отдельных workers (ThreadPoolForegroundWorker), которые вместе с основным потоком и моим собственным worker дают пять потоков. + +Я предполагаю, что это вызывает оставшиеся нарушения рендеринга на моём четырёхъядерном процессоре, поскольку я вижу необъяснимо долгие времена в функции Performance в DevTools Chrome. + +Итак, могу ли я как-то ограничить количество workers, используемых createImageBitmap? Даже если я передаю изображения как blobs или array buffers в основной поток и активирую createImageBitmap оттуда, его workers будут конкурировать с моим собственным worker и основным потоком. + +Я пробовал создавать обычные Images в worker вместо этого, чтобы явно декодировать их там, но Image не определён в контексте worker, как и document, если я хочу создать их как элементы. И обычные Images тоже не передаваемые, так что создание их в основном потоке и передача в worker тоже не кажется осуществимой. + +Жду любых предложений... + +## Ответ: + +Нет причин использовать createImageBitmap в worker (ну, см. внизу). Браузер уже декодирует изображение в отдельном потоке. Делать это в worker ничего не даёт. Большая проблема в том, что ImageBitmap не может знать, как вы собираетесь использовать изображение, когда наконец передадите его в WebGL. Если вы запросите формат, отличный от того, что декодировал ImageBitmap, то WebGL должен конвертировать и/или декодировать его снова, и вы не можете дать ImageBitmap достаточно информации, чтобы сказать ему, в каком формате вы хотите его декодировать. + +Кроме того, WebGL в Chrome должен передавать данные изображения из процесса рендеринга в GPU процесс, что для большого изображения является относительно большой копией (1024x1024 по RGBA это 4 мега) + +Лучший API, на мой взгляд, позволил бы вам сказать ImageBitmap, какой формат вы хотите и где вы хотите его (CPU, GPU). Таким образом браузер мог бы асинхронно подготовить изображение и не требовал бы тяжёлой работы при завершении. + +В любом случае, вот тест. Если вы снимите галочку "update texture", то текстуры всё ещё скачиваются и декодируются, но просто не вызывается `gl.texImage2D` для загрузки текстуры. В этом случае я не вижу задержек (не доказательство, что это проблема, но я думаю, что проблема там) + +{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-1.html"}}} + +Я довольно уверен, что единственный способ, которым вы могли бы гарантировать отсутствие задержек — это декодировать изображения самостоятельно в worker, передавать в основной поток как arraybuffer и загружать в WebGL по несколько строк за кадр с `gl.bufferSubData`. + +{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-2.html"}}} + +Примечание: Я не знаю, что это тоже будет работать. Несколько мест, которые пугают и зависят от реализации браузера + +1. Какие проблемы производительности при изменении размера canvas. Код изменяет размер OffscreenCanvas в worker. Это может быть тяжёлая операция с последствиями для GPU. + +2. Какая производительность рисования bitmap в canvas? Снова большие проблемы производительности GPU, поскольку браузер должен передать изображение на GPU, чтобы нарисовать его в GPU 2D canvas. + +3. Какая производительность getImageData? Снова браузер должен потенциально заморозить GPU, чтобы прочитать память GPU и получить данные изображения. + +4. Возможный удар по производительности при изменении размера текстуры. + +5. Только Chrome в настоящее время поддерживает OffscreenCanvas + +1, 2, 3 и 5 могут быть решены декодированием [jpg](https://github.com/notmasteryet/jpgjs), [png](https://github.com/arian/pngjs) изображения самостоятельно, хотя действительно отстойно, что браузер имеет код для декодирования изображения, просто вы не можете получить доступ к коду декодирования никаким полезным способом. + +Для 4, если это проблема, это может быть решено выделением текстуры максимального размера изображения и затем копированием меньших текстур в прямоугольную область. Предполагая, что это проблема + +{{{example url="../webgl-qna-how-to-load-images-in-the-background-with-no-jank-example-3.html"}}} + +примечание: jpeg декодер медленный. Если найдёте или сделаете более быстрый, пожалуйста, оставьте комментарий + +--- + +# Обновление + +Я просто хочу сказать, что `ImageBitmap` **должен** быть достаточно быстрым и что некоторые из моих комментариев выше о том, что у него недостаточно информации, могут быть не совсем правильными. + +Моё текущее понимание в том, что вся суть `ImageBitmap` была в том, чтобы сделать загрузки быстрыми. Это должно работать так: вы даёте ему blob и асинхронно он загружает это изображение в GPU. Когда вы вызываете `texImage2D` с ним, браузер может "blit" (рендерить с GPU) это изображение в вашу текстуру. Я не знаю, почему есть задержки в первом примере, учитывая это, но я вижу задержки каждые 6 или около того изображений. + +С другой стороны, хотя загрузка изображения в GPU была целью `ImageBitmap`, браузеры не обязаны загружать в GPU. `ImageBitmap` всё ещё должен работать, даже если у пользователя нет GPU. Суть в том, что браузер решает, как реализовать функцию, и будет ли она быстрой или медленной, без задержек или с ними — полностью зависит от браузера. + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + Andreas Ekstrand + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md b/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md new file mode 100644 index 000000000..cd9169b2c --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-make-a-smudge-brush-tool.md @@ -0,0 +1,54 @@ +Title: Как сделать инструмент размазывающей кисти +Description: Как сделать инструмент размазывающей кисти +TOC: Как сделать инструмент размазывающей кисти + +## Вопрос: + +Мне нужна идея, как я могу сделать кисть, которая может размазывать цвет. + +Пример на картинке: правая сторона - рисование базовой кистью с двумя разными цветами, слева также рисование, но дополнительно используется инструмент размазывания, **результат должен быть чем-то вроде левой стороны** + +![enter image description here][1] + +Мне нужен совет, как я могу попробовать это сделать + + [1]: http://i.stack.imgur.com/oyaBs.png + +## Ответ: + +Вот одна попытка + +1. При mousedown захватить копию области под мышью в отдельный canvas + +2. При mousemove рисовать эту копию по одному пикселю за раз от предыдущей позиции мыши к текущей позиции мыши с 50% прозрачностью, захватывая новую копию после каждого движения. + +В псевдокоде + +``` +on mouse down + grab copy of canvas at mouse position + prevMousePos = currentMousePos + +on mouse move + for (pos = prevMousePos to currentMousePos step 1 pixel) + draw copy at pos with 50% alpha + grab new copy of canvas at pos + prevMousePos = currentMousePos +``` + +Кисть размывается путем рисования радиального градиента от rgba(0,0,0,0) до rgba(0,0,0,1) над ней, используя `globalCompositeOperation = 'destination-out'`. + + +{{{example url="../webgl-qna-how-to-make-a-smudge-brush-tool-example-1.html"}}} + + + + +
+
Вопрос и процитированные части являются + CC BY-SA 3.0 от + Your choice + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md b/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md new file mode 100644 index 000000000..7ef38b3ac --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-make-webgl-canvas-transparent.md @@ -0,0 +1,62 @@ +Title: Как сделать canvas WebGL прозрачным +Description: Как сделать canvas WebGL прозрачным +TOC: Как сделать canvas WebGL прозрачным + +## Вопрос: + +Можно ли сделать **WebGL canvas** с прозрачным фоном? +Я хочу, чтобы содержимое веб-страницы было видно сквозь canvas. + +Вот что у меня сейчас: http://i50.tinypic.com/2vvq7h2.png + +Как видно, текст за WebGL canvas не виден. Когда я меняю стиль элемента Canvas в CSS и добавляю + + opacity: 0.5; + +Страница выглядит так: +https://imgur.com/BMgHWsZ + +Что почти то, что мне нужно, но не совсем — цвет текста из-за CSS alpha, конечно, не такой же черный, а цвет синей фигуры не такой же синий, как на первом изображении. + +Спасибо за любую помощь! +https://imgur.com/hSu5tyM + +## Ответ: + +WebGL по умолчанию поддерживает прозрачность. Вот пример + +{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-1.html"}}} + +Обратите внимание, что браузер предполагает, что пиксели в canvas представлены в формате PRE-MULTIPLIED-ALPHA. Это значит, например, если вы измените цвет очистки на (1, 0, 0, 0.5), вы получите то, что не увидите больше нигде в HTML. + +Я имею в виду, что pre-multiplied alpha означает, что RGB-части уже умножены на alpha. То есть если у вас 1,0,0 для RGB и alpha 0.5, то после умножения RGB на alpha получится 0.5, 0, 0 для RGB. Именно это браузер ожидает по умолчанию. + +Если пиксели в WebGL — 1,0,0,0.5, это не имеет смысла для браузера, и вы получите странные эффекты. + +Смотрите, например: + +{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-2.html"}}} + +Обратите внимание, что черный текст стал красным, хотя вы могли бы подумать, что alpha 0.5 = 50% черного текста и 50% красного WebGL canvas. Это потому, что красный не был pre-multiplied. + +Вы можете решить это, убедившись, что значения, которые вы создаете в WebGL, представляют собой pre-multiplied значения, или вы можете сказать браузеру, что ваши пиксели WebGL не pre-multiplied, когда создаете контекст webgl так: + + const gl = canvas.getContext("webgl", { premultipliedAlpha: false }); + +Теперь пиксели 1,0,0,0.5 снова работают. Пример: + +{{{example url="../webgl-qna-how-to-make-webgl-canvas-transparent-example-3.html"}}} + +Какой способ использовать — зависит от вашего приложения. Многие GL-программы ожидают не pre-multiplied alpha, тогда как все остальные части HTML5 ожидают pre-multiplied alpha, поэтому WebGL дает вам оба варианта. + + + + +
+
Вопрос и процитированные части являются + CC BY-SA 3.0 от + Jack Sean + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md b/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md new file mode 100644 index 000000000..1ed856cc1 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-optimize-rendering-a-ui.md @@ -0,0 +1,115 @@ +Title: Как оптимизировать рендеринг UI +Description: Как оптимизировать рендеринг UI +TOC: Как оптимизировать рендеринг UI + +## Вопрос: + +Я только начинаю с WebGL. Я следовал простому руководству для начинающих на YouTube. Теперь я пытаюсь создать простую 2D игру. + +В этой игре я хочу рендерить простой инвентарь с изображениями. Когда я это делаю, мой FPS падает до 2 после 10 секунд. Если я убираю код для рендеринга инвентаря, он остаётся на 60. + +Я знаю, что моя проблема в строке 82 в `game/js/engine/inventory/inventory.js`. Там я рендерю 35 изображений с классом sprite, который я сделал, смотря руководство. Я думаю, что поскольку я смотрел простое руководство, код, который рендерит изображение, не оптимизирован и, вероятно, не лучший способ это делать. Класс sprite находится в `game/js/engine/material.js:127`. В классе sprite я настраиваю простые переменные, которые можно передать в мой вершинный и фрагментный шейдер. + +## Настройка Sprite ## +В методе setup я настраиваю все параметры для моего изображения. +``` +gl.useProgram(this.material.program); + +this.gl_tex = gl.createTexture(); + +gl.bindTexture(gl.TEXTURE_2D, this.gl_tex); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image); +gl.bindTexture(gl.TEXTURE_2D, null); + +this.uv_x = this.size.x / this.image.width; +this.uv_y = this.size.y / this.image.height; + +this.tex_buff = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, this.tex_buff); +gl.bufferData(gl.ARRAY_BUFFER, Sprite.createRenderRectArray(0, 0, this.uv_x, this.uv_y), gl.STATIC_DRAW); + +this.geo_buff = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, this.geo_buff); +gl.bufferData(gl.ARRAY_BUFFER, Sprite.createRectArray(0, 0, this.size.x, this.size.y), gl.STATIC_DRAW); + +gl.useProgram(null); +``` + +## Рендеринг Sprite ## +В методе render я сначала привязываю текстуру. Затем привязываю буфер текстурных координат, геометрический буфер и некоторые смещения для моего мира. Наконец, рисую массивы. +``` +let frame_x = Math.floor(frames.x) * this.uv_x; +let frame_y = Math.floor(frames.y) * this.uv_y; + +let oMat = new M3x3().transition(position.x, position.y); +gl.useProgram(this.material.program); + +this.material.set("u_color", 1, 1, 1, 1); + +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, this.gl_tex); +this.material.set("u_image", 0); + +gl.bindBuffer(gl.ARRAY_BUFFER, this.tex_buff); +this.material.set("a_texCoord"); + +gl.bindBuffer(gl.ARRAY_BUFFER, this.geo_buff); +this.material.set("a_position"); + +this.material.set("u_texeloffset", 0.5 / (this.image.width * scale.x), 0.5 / (this.image.height * scale.y)); +this.material.set("u_frame", frame_x, frame_y); +this.material.set("u_world", worldSpaceMatrix.getFloatArray()); +this.material.set("u_object", oMat.getFloatArray()); + +gl.drawArrays(gl.TRIANGLE_STRIP, 0, 6); +gl.useProgram(null); +``` +Github: [https://github.com/DJ1TJOO/2DGame/][1] + +У кого-нибудь есть идея, как я могу исправить/оптимизировать это? +Или может быть есть лучший способ рендерить инвентарь? + +Если вы найдёте любой другой способ улучшить мой WebGL или JavaScript, пожалуйста, скажите. + +[1]: https://github.com/DJ1TJOO/2DGame/ + +## Ответ: + +> есть лучший способ рендерить инвентарь? + +Есть несколько способов оптимизации, которые приходят в голову. + +1. Может быть быстрее просто использовать HTML для вашего инвентаря + + Серьёзно: Вы также получаете простое международное рендеринг шрифтов, стили, + отзывчивость с CSS и т.д... Много игр делают это. + +2. Обычно быстрее использовать texture atlas (одну текстуру с множеством разных изображений), затем генерировать вершины в vertex buffer для всех частей вашего инвентаря. Затем рисовать всё одним вызовом draw. Так работает, например, [Dear ImGUI](https://github.com/ocornut/imgui), чтобы делать [все эти удивительные GUI](https://github.com/ocornut/imgui/issues/3075). Он сам ничего не рисует, он просто генерирует vertex buffer с позициями и текстурными координатами для texture atlas. + +3. Делайте #2, но вместо генерации всего vertex buffer каждый кадр просто обновляйте части, которые изменяются. + + Так, например, допустим ваш инвентарь показывает + + [gold ] 123 + [silver] 54 + [copper] 2394 + + Допустим, вы всегда рисуете `[gold ]`, `[silver]` и `[copper]`, но только числа изменяются. Вы могли бы генерировать vertex buffers, которые содержат все позиции для каждой буквы как sprite, и затем сказать 6 placeholder'ов символов для каждого значения. Вам нужно обновлять только числа, когда они изменяются, помня, где они находятся в vertex buffers. Для любой цифры, которую вы не хотите рисовать, вы можете просто переместить её вершины за экран. + +4. Рисуйте инвентарь в текстуру (или части его). Затем рисуйте текстуру на экране. Обновляйте только части текстуры, которые изменяются. + + Это в основном [то, что делает сам браузер](https://www.html5rocks.com/en/tutorials/speed/layers/). На основе различных настроек CSS части страницы разделяются на текстуры. Когда какой-то HTML или CSS изменяется, только те текстуры, в которых что-то изменилось, перерисовываются, а затем все текстуры рисуются для рекомпозиции страницы. + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + DJ1TJOO + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md b/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md new file mode 100644 index 000000000..7c462415a --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.md @@ -0,0 +1,125 @@ +Title: Как предотвратить просачивание текстур с атласом текстур +Description: Как предотвратить просачивание текстур с атласом текстур +TOC: Как предотвратить просачивание текстур с атласом текстур + +## Вопрос: + +Я применил два необходимых шага, указанных в этом ответе https://gamedev.stackexchange.com/questions/46963/how-to-avoid-texture-bleeding-in-a-texture-atlas, но я всё ещё получаю просачивание текстур. + +У меня есть атлас, который заполнен сплошными цветами на границах: `x y w h: 0 0 32 32, 0 32 32 32, 0 64 32 32, 0 32 * 3 32 32` + +Я хочу отображать каждый из этих кадров, используя WebGL без просачивания текстур, только сплошные цвета как есть. + +Я отключил мипмаппинг: + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + + //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + +Я применил коррекцию полпикселя: + + const uvs = (src, frame) => { + const tw = src.width, + th = src.height; + + const getTexelCoords = (x, y) => { + return [(x + 0.5) / tw, (y + 0.5) / th]; + }; + + let frameLeft = frame[0], + frameRight = frame[0] + frame[2], + frameTop = frame[1], + frameBottom = frame[1] + frame[3]; + + let p0 = getTexelCoords(frameLeft, frameTop), + p1 = getTexelCoords(frameRight, frameTop), + p2 = getTexelCoords(frameRight, frameBottom), + p3 = getTexelCoords(frameLeft, frameBottom); + + return [ + p0[0], p0[1], + p1[0], p1[1], + p3[0], p3[1], + p2[0], p2[1] + ]; + }; + +Но я всё ещё получаю просачивание текстур. Сначала я попробовал использовать pixi.js, и я тоже получил просачивание текстур, затем я попробовал использовать vanilla js. + +Я исправил это, изменив эти строки: + + let frameLeft = frame[0], + frameRight = frame[0] + frame[2] - 1, + frameTop = frame[1], + frameBottom = frame[1] + frame[3] - 1; + +Как вы можете видеть, я вычитаю 1 из правого и нижнего краёв. Ранее эти индексы были 32, что означает начало другого кадра, это должно быть 31 вместо этого. Я не знаю, является ли это правильным решением. + +## Ответ: + +Ваше решение правильно. + +Представьте, что у нас есть текстура 4x2 с двумя спрайтами 2x2 пикселя + +``` ++-------+-------+-------+-------+ +| | | | | +| E | F | G | H | +| | | | | ++-------+-------+-------+-------+ +| | | | | +| A | B | C | D | +| | | | | ++-------+-------+-------+-------+ +``` + +Буквы представляют центры пикселей в текстурах. + +``` +(pixelCoord + 0.5) / textureDimensions +``` + +Возьмите спрайт 2x2 в A, B, E, F. Если ваши текстурные координаты идут где-либо между B и C, то вы получите некоторую смесь C, если у вас включена фильтрация текстур. + +Изначально вы вычисляли координаты A, A + width, где width = 2. Это привело вас от A до C. Добавив -1, вы получаете только от A до B. + +К сожалению, у вас есть новая проблема, которая заключается в том, что вы отображаете только половину A и B. Вы можете решить это, добавив отступы к спрайтам. Например, сделайте его 6x2 с пикселем между ними, который повторяет края спрайта + +``` ++-------+-------+-------+-------+-------+-------+ +| | | | | | | +| E | F | Fr | Gr | G | H | +| | | | | | | ++-------+-------+-------+-------+-------+-------+ +| | | | | | | +| A | B | Br | Cr | C | D | +| | | | | | | ++-------+-------+-------+-------+-------+-------+ +``` + +Выше Br - это B повторённый, Cr - это C повторённый. Установка repeat как `gl.CLAMP_TO_EDGE` повторит A и D для вас. Теперь вы можете использовать края. + +Координаты спрайта CDGH: + + p0 = 4 / texWidth + p1 = 0 / texHeigth + p2 = (4 + spriteWidth) / texWidth + p3 = (0 + spriteHeigth) / texHeight + +Лучший способ увидеть разницу - нарисовать 2 спрайта крупно, используя обе техники, без отступов и с отступами. + +{{{example url="../webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas-example-1.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + eguneys + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md b/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md new file mode 100644 index 000000000..ff3cf2acb --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-process-particle-positions.md @@ -0,0 +1,42 @@ +Title: Как обрабатывать позиции частиц +Description: Как обрабатывать позиции частиц +TOC: Как обрабатывать позиции частиц + +## Вопрос: + +Используя сетку 4x4x4 в качестве примера, у меня есть 64 вершины (которые я буду называть частицами), которые начинают с определённых позиций относительно друг друга. Эти 64 частицы будут двигаться в направлениях x, y и z, теряя свои начальные позиции относительно друг друга. Однако каждый цикл новые позиции частиц и скорости должны вычисляться на основе исходных начальных отношений между частицей и её исходными соседями. + +Я узнал, что мне нужно использовать текстуры и, следовательно, Framebuffers для этого, и теперь я могу записывать две 3DTextures, которые переключаются, чтобы обеспечить функциональность записи и чтения для выполнения этого. Однако в следующем цикле, когда gl_FragCoord передаётся в фрагментный шейдер с новой позицией частицы (которая может быть переключена с другой частицей, например), я не вижу никакого механизма, с помощью которого исходная координата текстуры, которая содержала информацию о частице, будет записана с текущей информацией частицы. Есть ли какой-то механизм, который я не понимаю, который позволяет движущимся частицам иметь свои данные, хранящиеся в статической сетке (3D текстура), с данными каждой частицы, всегда заполняющими ту же координату, чтобы я мог использовать texelFetch для получения данных частицы, а также данных исходных соседей? Могу ли я изменить gl_FragCoord и иметь вывод пикселя там, где я хочу, или это неизменяемая входная переменная? + +Как только я решу эту проблему, я надеюсь затем реализовать Transform Feedback для выполнения фактического движения вершин без сброса текстуры в CPU и извлечения данных позиции и повторной загрузки их в GPU для следующего цикла. + +Есть ли какие-либо предложения о том, как отслеживать исходную позицию каждой частицы, исходных соседей и текущую позицию относительно этих исходных соседей, используя текстуры, записанные в Framebuffers? + +## Ответ: + +Я запутался в вашей путанице + +Вот простая система частиц только на JavaScript. Каждая частица начинается в случайном месте и движется в случайном направлении + +{{{example url="../webgl-qna-how-to-process-particle-positions-example-1.html"}}} + +Вот та же система частиц, всё ещё в JavaScript, но работающая больше как работает WebGL. Я не знаю, будет ли это более или менее запутанно. Важные моменты заключаются в том, что код, который обновляет позиции частиц, называемый `fragmentShader`, не может выбирать, что он обновляет. Он просто обновляет `gl.outColor`. У него также нет входов, кроме `gl.fragCoord` и `gl.currentProgram.uniforms`. currentParticleState - это массив массивов из 4 значений, где, как и раньше, это был массив объектов со свойством position. particleParameters также просто массив массивов из 4 значений вместо массива объектов со значением velocity. Это для имитации того факта, что это были бы текстуры в реальном WebGL, поэтому любое значение, такое как `position` или `velocity`, теряется. + +Код, который фактически рисует частицы, не имеет значения. + +{{{example url="../webgl-qna-how-to-process-particle-positions-example-2.html"}}} + +Вот тот же код в реальном WebGL + +{{{example url="../webgl-qna-how-to-process-particle-positions-example-3.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + billvh + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md b/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md new file mode 100644 index 000000000..a6df3a16c --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-read-a-single-component-with-readpixels.md @@ -0,0 +1,295 @@ +Title: Как читать один компонент с помощью readPixels +Description: Как читать один компонент с помощью readPixels +TOC: Как читать один компонент с помощью readPixels + +## Вопрос: + +Я конвертировал RGBA изображение в оттенки серого с помощью WebGL. + +При чтении пикселей с помощью `gl.readPixels()` с форматом `gl.RGBA` получаю значения для каждого пикселя как YYYA, потому что RGBA пиксель конвертируется в YYYA и присваивается `gl_FragColor`. Я хочу только 1 байт Y-компонента для каждого пикселя вместо 4 байт. + +Попробовал читать пиксели с форматом `gl.RED` (вместо `gl.RGBA`) + +``` +gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RED, gl.UNSIGNED_BYTE, pixels); +``` + +но получаю следующую ошибку в Chrome и только нули: + +``` +WebGL: INVALID_ENUM: readPixels: invalid format +``` + +1. Можно ли заставить `gl_FragColor` выводить 1 байт на пиксель в режиме LUMINANCE вместо RGBA, но входная текстура должна быть RGBA? +2. Если формат рендеринга gl нельзя изменить, можно ли читать только первый байт каждого 4-байтового пикселя при вызове `gl.readPixels()`? + +Примечание: +3. Я уже делаю копию вывода `gl.readPixels()` в другой массив, перепрыгивая каждые 4 байта. Но хочу избежать этой копии, так как это занимает больше времени. +4. Также нужно, чтобы решение было совместимо с мобильными браузерами (iOS Safari и Android Chrome). + + + + + + function webGL() { + var gTexture; + var gFramebuffer; + var srcCanvas = null; + var programs = {}; + var program; + var pixels; + + this.convertRGBA2Gray = function(inCanvas, inArray) { + // Y компонент из YCbCr + const shaderSourceRGB2GRAY = ` + precision mediump float; + + uniform sampler2D u_image; + uniform vec2 u_textureSize; + vec4 scale = vec4(0.299, 0.587, 0.114, 0.0); + void main() { + vec4 color = texture2D(u_image, gl_FragCoord.xy / u_textureSize); + gl_FragColor = vec4(vec3(dot(color,scale)), color.a); + }`; + + if (srcCanvas === null) { + console.log('Setting up webgl'); + srcCanvas = inCanvas; + _initialize(srcCanvas.width, srcCanvas.height); + program = _createProgram("rgb2grey", shaderSourceRGB2GRAY); + } + pixels = inArray; + _run(program); + } + + /////////////////////////////////////// + // Приватные функции + + var _getWebGLContext = function(canvas) { + try { + return (canvas.getContext("webgl", {premultipliedAlpha: false, preserveDrawingBuffer: true}) || canvas.getContext("experimental-webgl", {premultipliedAlpha: false, preserveDrawingBuffer: true})); + } + catch(e) { + console.log("ERROR: %o", e); + } + return null; + } + + var gl = _getWebGLContext(document.createElement('canvas')); + + var _initialize = function(width, height) { + var canvas = gl.canvas; + canvas.width = width; + canvas.height = height; + + if (this.originalImageTexture) { + return; + } + + this.originalImageTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + gTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, gTexture); + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + + gFramebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, gFramebuffer); + + var positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1.0, 1.0, + 1.0, 1.0, + 1.0, -1.0, + + -1.0, 1.0, + 1.0, -1.0, + -1.0, -1.0 + ]), gl.STATIC_DRAW); + + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, gTexture, 0); + + gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, srcCanvas); + } + + var _createProgram = function(name, fragmentSource, vertexSource) { + shaderProgram = programs[name]; + + if (shaderProgram){ + console.log('Reusing program'); + gl.useProgram(shaderProgram); + return shaderProgram; + } + + function createShader(type, source){ + var shader = gl.createShader(type); + + gl.shaderSource(shader, source); + + gl.compileShader(shader); + + return shader; + } + + var vertexShader, fragmentShader; + + if (!vertexSource){ + vertexShader = createShader(gl.VERTEX_SHADER, `attribute vec2 a_position; + void main() { gl_Position = vec4(a_position, 0.0, 1.0); }` + ); + } else { + vertexShader = createShader(gl.VERTEX_SHADER, vertexSource); + } + fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource); + + shaderProgram = gl.createProgram(); + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + + gl.useProgram(shaderProgram); + return shaderProgram; + } + + var _render = function(gl, program){ + var positionLocation = gl.getAttribLocation(program, "a_position"); + + var u_imageLoc = gl.getUniformLocation(program, "u_image"); + var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize"); + + gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(positionLocation); + + var width = gl.canvas.width, + height = gl.canvas.height; + + gl.bindFramebuffer(gl.FRAMEBUFFER, gFramebuffer); + + gl.uniform2f(textureSizeLocation, width, height); + + gl.uniform1i(u_imageLoc, 0); + + gl.viewport(0, 0, width, height); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + + } + + var _run = function(program){ + let t0 = performance.now(); + _render(gl, program); + gl.bindTexture(gl.TEXTURE_2D, gTexture); + let t1 = performance.now(); + + // gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RED, gl.UNSIGNED_BYTE, pixels); + + let t2 = performance.now(); + console.log('_render dur = ' + Number((t1-t0).toFixed(3)) + ' ms'); + console.log('_run dur = ' + Number((t2-t0).toFixed(3)) + ' ms'); + } + + }; + + + + +
+ +
+ + + + + + + + + + +## Ответ: + +> Можно ли заставить gl_FragColor выводить 1 байт на пиксель в режиме LUMINANCE вместо RGBA, но входная текстура должна быть RGBA? + +Не переносимо. Спецификация WebGL1 говорит, что рендеринг в текстуру должен поддерживаться только для gl.RGBA / gl.UNSIGNED_BYTE. Все остальные форматы опциональны. + +> Если формат рендеринга gl нельзя изменить, можно ли читать только первый байт каждого 4-байтового пикселя при вызове gl.readPixels()? + +Нет, [Спецификация](https://www.khronos.org/registry/OpenGL/specs/es/2.0/es_full_spec_2.0.pdf) раздел 4.3.1 говорит, что поддерживается только `gl.RGBA`, `gl.UNSIGNED_BYTE`. Все остальные форматы опциональны и зависят от реализации. То же самое в WebGL2. Даже если вы создадите R8 текстуру (только красный, 8 бит), зависит от реализации, можете ли вы читать её как `gl.RED`/`gl.UNSIGNED_BYTE`. + +Смотрите [Webgl1](https://webglfundamentals.org/webgl/lessons/webgl-readpixels.html) и [Webgl2](https://webgl2fundamentals.org/webgl/lessons/webgl-readpixels.html) + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + rayen + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md b/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md new file mode 100644 index 000000000..e61c45f9d --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-render-large-scale-images-like-32000x32000.md @@ -0,0 +1,113 @@ +Title: Как рендерить изображения большого масштаба как 32000x32000 +Description: Как рендерить изображения большого масштаба как 32000x32000 +TOC: Как рендерить изображения большого масштаба как 32000x32000 + +## Вопрос: + +Я хочу получить снимок моего WebGL canvas, и я хочу высокое разрешение, поэтому я увеличил размер моего canvas. Это автоматически изменяет `gl.drawingBufferWidth` и `gl.drawingBufferHeight`. Затем я устанавливаю viewport и рендерю сцену. + +Мой код работает правильно в низком разрешении (под 4000*4000), но в более высоких разрешениях есть много проблем. + +Если разрешение немного выше, снимок не показывает полностью. См. прикреплённый файл. Если разрешение увеличивается больше, ничего не показывается. И наконец, при некоторых разрешениях мой экземпляр WebGL уничтожается, и мне приходится перезапускать браузер, чтобы снова запустить WebGL. + +Есть ли способ получить снимок с WebGL canvas с высоким разрешением? Могу ли я использовать другое решение? + +## Ответ: + +4000x4000 пикселей - это 4000x4000x4 или 64 мегабайта памяти. 8000x8000 - это 256 мегабайт памяти. Браузеры не любят выделять такие большие куски памяти и часто устанавливают лимиты на страницу. Так, например, у вас есть WebGL canvas 8000x8000, который требует 2 буфера. Drawingbuffer И текстура, отображаемая на странице. Drawingbuffer может быть сглажен. Если это 4x MSAA, то потребуется гигабайт памяти только для этого буфера. Затем вы делаете скриншот, так что ещё 256 мегабайт памяти. Так что да, браузер по той или иной причине, скорее всего, убьёт вашу страницу. + +Помимо этого, WebGL имеет свои собственные лимиты в размере. Вы можете посмотреть этот лимит, который эффективно [`MAX_TEXTURE_SIZE`](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE) или [`MAX_VIEWPORT_DIMS`](https://web3dsurvey.com/webgl/parameters/MAX_VIEWPORT_DIMS). Вы можете увидеть из тех, что около 40% машин не могут рисовать больше 4096 (хотя если вы [отфильтруете только десктоп, это намного лучше](https://web3dsurvey.com/webgl/parameters/MAX_VIEWPORT_DIMS?platforms=0000ff03c02d20f201)). Это число означает только то, что может делать оборудование. Оно всё ещё ограничено памятью. + +Один способ, который может решить эту проблему, - рисовать изображение по частям. Как вы это делаете, будет зависеть от вашего приложения. Если вы используете довольно стандартную матрицу перспективы для всего вашего рендеринга, вы можете использовать немного другую математику для рендеринга любой части вида. Большинство 3D математических библиотек имеют функцию `perspective`, и большинство из них также имеют соответствующую функцию `frustum`, которая немного более гибкая. + +Вот довольно стандартный стиль WebGL простой пример, который рисует куб, используя типичную функцию `perspective` + +{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-1.html"}}} + +И вот тот же код, рендерящий в 400x200 в восьми частях 100x100, используя типичную функцию `frustum` вместо `perspective` + +{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-2.html"}}} + +Если вы запустите фрагмент выше, вы увидите, что он генерирует 8 изображений + +Важные части - это + +Сначала нам нужно решить общий размер, который мы хотим + + const totalWidth = 400; + const totalHeight = 200; + +Затем мы создадим функцию, которая будет рендерить любую меньшую часть этого размера + + function renderPortion(totalWidth, totalHeight, partX, partY, partWidth, partHeight) { + ... + +Мы установим canvas на размер части + + gl.canvas.width = partWidth; + gl.canvas.height = partHeight; + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + +И затем вычислим, что нам нужно передать в функцию `frustum`. Сначала мы вычисляем прямоугольник на zNear, который матрица перспективы создала бы, учитывая наши значения field of view, aspect и zNear + + // углы на zNear для общего изображения + const zNearTotalTop = Math.tan(fov) * 0.5 * zNear; + const zNearTotalBottom = -zNearTotalTop; + const zNearTotalLeft = zNearTotalBottom * aspect; + const zNearTotalRight = zNearTotalTop * aspect; + + // ширина, высота на zNear для общего изображения + const zNearTotalWidth = zNearTotalRight - zNearTotalLeft; + const zNearTotalHeight = zNearTotalTop - zNearTotalBottom; + +Затем мы вычисляем соответствующую область на zNear для части, которую мы хотим рендерить, и передаём их в `frustum` для генерации матрицы проекции. + + const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth; const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth; + const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight; + const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight; + + const projection = m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar); + +Затем мы просто рендерим как обычно + +Наконец, снаружи у нас есть цикл для использования функции, которую мы только что сгенерировали, чтобы рендерить столько частей, сколько мы хотим, с любым разрешением, которое мы хотим. + + const totalWidth = 400; + const totalHeight = 200; + const partWidth = 100; + const partHeight = 100; + + for (let y = 0; y < totalHeight; y += partHeight) { + for (let x = 0; x < totalWidth; x += partWidth) { + renderPortion(totalWidth, totalHeight, x, y, partWidth, partHeight); + const img = new Image(); + img.src = gl.canvas.toDataURL(); + // сделать что-то с изображением. + } + } + +Это позволит вам рендерить в любой размер, который вы хотите, но вам понадобится другой способ собрать изображения в одно большее изображение. Вы можете или не можете сделать это в браузере. Вы можете попробовать создать гигантский 2D canvas и рисовать каждую часть в него (это предполагает, что 2D canvas не имеет тех же лимитов, что и WebGL). Для этого нет необходимости создавать изображения, просто рисуйте WebGL canvas в 2D canvas. + +В противном случае вам, возможно, придётся отправить их на сервер, который вы создадите, чтобы собрать изображение, или в зависимости от вашего случая использования позволить пользователю сохранить их и загрузить их все в программу редактирования изображений. + +Или если вы просто хотите отобразить их, браузер, вероятно, будет лучше работать с 16x16 изображениями 1024x1024, чем с одним изображением 16kx16k. В этом случае вы, вероятно, хотите вызвать `canvas.toBlob` вместо использования dataURL и затем вызвать `URL.createObjectURL` для каждого blob. Таким образом, у вас не будет этих гигантских строк dataURL. + +Пример: + +{{{example url="../webgl-qna-how-to-render-large-scale-images-like-32000x32000-example-3.html"}}} + +Если вы хотите, чтобы пользователь мог скачать изображение 16386x16386 вместо 256 изображений 1024x1024, то ещё одно решение - использовать код рендеринга частей выше и для каждой строки (или строк) изображений записать их данные в blob для ручной генерации PNG. [Этот пост в блоге](https://medium.com/the-guardian-mobile-innovation-lab/generating-images-in-javascript-without-using-the-canvas-api-77f3f4355fad) охватывает ручную генерацию PNG из данных, и [этот ответ предлагает, как сделать это для очень больших данных](https://stackoverflow.com/a/51247740/128511). + +## обновление: + +Просто для развлечения я написал эту [библиотеку, чтобы помочь генерировать гигантские PNG в браузере](https://github.com/greggman/dekapng). + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + MHA15 + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md b/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md new file mode 100644 index 000000000..000bf26db --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-simulate-a-3d-texture-in-webgl.md @@ -0,0 +1,105 @@ +Title: Как симулировать 3D текстуру в WebGL +Description: Как симулировать 3D текстуру в WebGL +TOC: Как симулировать 3D текстуру в WebGL + +## Вопрос: + +Итак, в WebGL я могу хранить текстуру максимум в 2 измерениях и читать её в шейдере с помощью texture2D(whatever); + +Если я хочу хранить 3-мерную текстуру, чтобы читать 3-мерные данные в шейдере, как это сделать? + +Вот мои идеи — и я хочу знать, правильно ли я подхожу к этому: + +В JavaScript: + + var info = []; + + for (var x = 0; x < 1; x+=.1) { + for (var y = 0; y < 1; y+=.1) { + for (var z = 0; z < 1; z+=.1) { + + info.push (x*y*z); + info.push(0); + info.push(0); + info.push(0); + + } + } + } + + //bind texture here- whatever + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 10, 100, 0, + gl.RGBA, gl.FLOAT, data_on_shader); + + //other texture stuff + +В шейдере: + + uniform sampler data_on_shader; + x= texture.r// + y = texture.g// + z = texture.b// + + xfixed = floor(x*10.)/10. + .5; + yfixed = floor(y*10.)/10. + .5; + zfixed = floor(z*10.)/10. + .5; + + float data_received = texture2D(data_on_shader, vec2(xfixed, yfixed*10. + zfixed)).r; + +Что-то вроде использования row major order в 2D текстуре? +Мысли? + +Спасибо заранее! + +## Ответ: + +Вы можете симулировать 3D текстуру, сохраняя каждую плоскость 3D текстуры в 2D текстуре + +Затем функция вроде этой позволит вам использовать её как 3D текстуру: + + vec4 sampleAs3DTexture(sampler2D tex, vec3 texCoord, float size) { + float sliceSize = 1.0 / size; // пространство 1 среза + float slicePixelSize = sliceSize / size; // пространство 1 пикселя + float sliceInnerSize = slicePixelSize * (size - 1.0); // пространство size пикселей + float zSlice0 = min(floor(texCoord.z * size), size - 1.0); + float zSlice1 = min(zSlice0 + 1.0, size - 1.0); + float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize; + float s0 = xOffset + (zSlice0 * sliceSize); + float s1 = xOffset + (zSlice1 * sliceSize); + vec4 slice0Color = texture2D(tex, vec2(s0, texCoord.y)); + vec4 slice1Color = texture2D(tex, vec2(s1, texCoord.y)); + float zOffset = mod(texCoord.z * size, 1.0); + return mix(slice0Color, slice1Color, zOffset); + } + +Если ваша 3D текстура была 8x8x8, то вы создаёте 2D текстуру 64x8 и помещаете каждую плоскость 3D текстуры в вашу 2D текстуру. Затем, зная, что изначально было 8x8x8, вы передаёте `8.0` для размера в `sampleAs3DTexture` + + precision mediump float; + uniform sampler2D u_my3DTexture; + varying vec3 v_texCoord; + + ... + + #define CUBE_SIZE 8.0 + + void main() { + gl_FragColor = sampleAs3DTexture(u_my3DTexture, v_texCoord, CUBE_SIZE); + } + +Примечание: функция выше предполагает, что вы хотите билинейную фильтрацию между плоскостями. Если не хотите, можете упростить функцию. + +Есть [видео-объяснение этого кода здесь][1], которое взято из [этого примера][2]. + + + [1]: http://www.youtube.com/watch?v=rfQ8rKGTVlg#t=26m00s + [2]: http://webglsamples.googlecode.com/hg/color-adjust/color-adjust.html + +
+
Вопрос и процитированные части являются + CC BY-SA 3.0 от + Skorpius + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md b/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md new file mode 100644 index 000000000..037476ad2 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-support-both-webgl-and-webgl2.md @@ -0,0 +1,97 @@ +Title: Как поддерживать и WebGL, и WebGL2 +Description: Как поддерживать и WebGL, и WebGL2 +TOC: Как поддерживать и WebGL, и WebGL2 + +## Вопрос: + +У меня есть библиотека, которая использует WebGL1 для рендеринга. +Она активно использует float-текстуры и instanced rendering. + +Сейчас поддержка WebGL1 довольно странная: некоторые устройства поддерживают, например, WebGL2 (где эти расширения встроены), но не поддерживают WebGL1, или поддерживают, но без нужных расширений. + +В то же время поддержка WebGL2 не идеальна. Возможно, когда-нибудь будет, но пока нет. + +Я начал думать, что потребуется для поддержки обеих версий. + +Для шейдеров, думаю, можно обойтись `#define`-ами. Например, `#define texture2D texture` и подобные вещи. + +С расширениями сложнее, так как объекты расширений больше не существуют. +В качестве эксперимента я пробовал копировать методы расширения в сам контекст, например: `gl.drawArraysInstanced = (...args) => ext.drawArraysInstancedANGLE(...args)`. + +С текстурами менять почти ничего не нужно, разве что добавить что-то вроде `gl.RGBA8 = gl.RGBA` при запуске в WebGL1, чтобы код "просто работал" и в WebGL2. + +Вопрос: кто-нибудь пробовал так делать? +Я опасаюсь, что это повредит производительности, особенно из-за лишней обёртки для вызова функций. +Это также усложнит чтение кода, если предполагать, что он может работать в WebGL1. Ведь в WebGL1 нет `drawArraysInstanced` или `RGBA8`. Это также мешает типизации в Typescript и другим мелочам. + +Второй вариант — делать ветвления по всему коду. Две версии шейдеров (или `#ifdef`-трюки), много ветвлений для каждого места, где нужны форматы текстур, и для каждого места, где используется instancing. +Вот так по всему коду — не очень красиво: + + if (version === 1) { + instancedArrays.vertexAttribDivisorANGLE(m0, 1); + instancedArrays.vertexAttribDivisorANGLE(m1, 1); + instancedArrays.vertexAttribDivisorANGLE(m2, 1); + instancedArrays.vertexAttribDivisorANGLE(m3, 1); + } else { + gl.vertexAttribDivisor(m0, 1); + gl.vertexAttribDivisor(m1, 1); + gl.vertexAttribDivisor(m2, 1); + gl.vertexAttribDivisor(m3, 1); + } + +Может, есть и третий способ, о котором я не подумал. + +Есть рекомендации? + +## Ответ: + +К сожалению, большинство ответов тут будут субъективными. + +Первый вопрос — зачем поддерживать оба? Если ваша идея работает в WebGL1 — просто используйте WebGL1. Если вам обязательно нужны фичи WebGL2 — используйте WebGL2 и смиритесь, что на старых устройствах не будет поддержки. + +Если вы всё же хотите поддерживать оба, [twgl](https://twgljs.org) пытается упростить это, [предлагая функцию, которая копирует все расширения WebGL1 в их WebGL2 API-позиции](http://twgljs.org/docs/module-twgl.html#.addExtensionsToContext). Как вы и упомянули, вместо + + ext = gl.getExtension('ANGLE_instanced_arrays'); + ext.drawArraysInstancedANGLE(...) + +Вы делаете + + twgl.addExtensionsToContext(gl); + gl.drawArraysInstanced(...); + +Я не думаю, что будет заметная разница в производительности. Эти функции вызываются всего несколько сотен раз за кадр, и обёртка не станет узким местом. + +Но суть не в том, чтобы поддерживать WebGL1 и WebGL2 одновременно. Скорее, чтобы писать код одинаково для обоих API. + +Тем не менее, есть реальные различия между API. Например, чтобы использовать FLOAT RGBA текстуру в WebGL1: + + gl.texImage2D(target, level, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, ...) + +В WebGL2: + + gl.texImage2D(target, level, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, ...) + +WebGL2 выдаст ошибку, если вызвать его так же, как WebGL1. [Есть и другие отличия](https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html). + +Впрочем, вот такой вызов будет работать и в WebGL1, и в WebGL2. Спецификация прямо говорит, что такая комбинация даёт RGBA8 в WebGL2. + +Обратите внимание, что ваш пример с RGBA8 не совсем верен. + + gl.texImage2D(target, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, ...) + +Главное отличие — нет смысла использовать WebGL2, если вам хватает WebGL1. Или наоборот: если вам нужен WebGL2, то откатиться на WebGL1 будет сложно. + +Вы упомянули define-ы для шейдеров, но что делать с фичами WebGL2, которых нет в WebGL1? Например, `textureFetch`, оператор `%`, integer attributes и т.д. Если они нужны — придётся писать отдельный шейдер только для WebGL2. Если не нужны — зачем тогда WebGL2? + +Конечно, если хочется, можно сделать более продвинутый рендерер для WebGL2 и попроще для WebGL1. + +TL;DR: На мой взгляд, выберите что-то одно. + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + user2503048 + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md b/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md new file mode 100644 index 000000000..dc9262c73 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.md @@ -0,0 +1,81 @@ +Title: Как определить, есть ли у изображения альфа-канал +Description: Как определить, есть ли у изображения альфа-канал +TOC: Как определить, есть ли у изображения альфа-канал + +## Вопрос: + +Когда вы загружаете текстуру WebGL напрямую из DOM-изображения, как узнать, есть ли у изображения альфа-канал? Можно ли это определить, кроме как угадывать по имени файла (например, если есть .PNG — возможно RGBA, иначе RGB)? В DOM-изображении есть ширина и высота, но я не вижу ничего, что бы говорило о формате. Например: + + const img = await loadDOMImage(url); + const format = gl.RGBA; //Это всегда должно быть RGBA? Я трачу память, если там только RGB + const internalFormat = gl.RGBA; + const type = gl.UNSIGNED_BYTE; //Это точно правильно? DOM не поддерживает HDR форматы? + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, img); + +Моя функция загрузки выглядит так: + + async loadDOMImage(url) { + return new Promise( + (resolve, reject)=>{ + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.addEventListener('load', function() { + resolve(img); + }, false); + img.addEventListener('error', function(err) { + reject(err); + }, false); + + img.src = uri; + + } + ); + } + + + + + +## Ответ: + +> как узнать, есть ли у изображения альфа-канал? + +Никак. Можно только догадываться. + +1. Можно посмотреть, заканчивается ли URL на .png, и предположить, что там есть альфа. Можно ошибиться. + +2. Можно нарисовать изображение в 2D canvas, затем вызвать getImageData, прочитать все альфа-значения и посмотреть, есть ли среди них не 255. + +> ``` +> const format = gl.RGBA; +> ``` +> Это всегда должно быть RGBA? Я трачу память, если там только RGB + +Скорее всего, память не тратится зря. Большинство GPU лучше работают с RGBA, так что даже если вы выберете RGB, это вряд ли сэкономит память. + +> ``` +> const type = gl.UNSIGNED_BYTE; +> ``` +> Это точно правильно? + +`texImage2D` берёт изображение, которое вы передаёте, и конвертирует его в `type` и `format`. Затем эти данные передаются на GPU. + +> DOM не поддерживает HDR форматы? + +Это не определено и зависит от браузера. Мне не известны HDR форматы, поддерживаемые браузерами. Какие форматы поддерживает браузер — решает сам браузер. + +--- + +Частый вопрос: нужно ли включать смешивание/прозрачность для конкретной текстуры. Некоторые ошибочно думают, что если у текстуры есть альфа, то надо, иначе — нет. Это неверно. Нужно ли использовать смешивание/прозрачность — это отдельный вопрос, не связанный с форматом изображения, и это нужно хранить отдельно. + +То же самое касается других вещей, связанных с изображениями. В каком формате загружать изображение в GPU — это зависит от приложения и задачи, и не связано с форматом самого файла изображения. + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + griffin2000 + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md b/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md new file mode 100644 index 000000000..0ec43ca64 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.md @@ -0,0 +1,88 @@ +Title: Как использовать прозрачность 2D-спрайта как маску +Description: Как использовать прозрачность 2D-спрайта как маску +TOC: Как использовать прозрачность 2D-спрайта как маску + +## Вопрос: + + ```javascript +if (statuseffect) { + // Очистка stencil buffer + gl.clearStencil(0); + gl.clear(gl.STENCIL_BUFFER_BIT); + + + gl.stencilFunc(gl.ALWAYS, 1, 1); + gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE); + + + gl.colorMask(false, false, false, false); + + gl.enable(gl.STENCIL_TEST); + + // Рисуем маску через gl.drawArrays L111 + drawImage(statuseffectmask.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4) + + // Говорим stencil теперь рисовать/оставлять только пиксели, равные 1 — что мы установили ранее + gl.stencilFunc(gl.EQUAL, 1, 1); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); + + // возвращаем обратно цветовой буфер + gl.colorMask(true, true, true, true); + + + drawImage(statuseffect.texture, lerp(-725, 675, this.Transtion_Value), 280, 128 * 4, 32 * 4) + + + gl.disable(gl.STENCIL_TEST); + } + + +``` +Я пытаюсь добиться такого эффекта [![enter image description here][1]][1] +Где берётся прозрачность спрайта, и затем другой спрайт рисуется только в областях, где нет прозрачности. Спасибо. + +[1]: https://i.stack.imgur.com/ESdGp.png + +## Ответ: + +Не совсем понятно, зачем вам использовать stencil для этого. Обычно вы бы просто [настроили смешивание (blending) и использовали прозрачность для смешивания](https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html). + +Если вы действительно хотите использовать stencil, вам нужно сделать шейдер, который вызывает `discard`, если прозрачность (alpha) меньше определённого значения, чтобы stencil устанавливался только там, где спрайт не прозрачный: + +``` +precision highp float; + +varying vec2 v_texcoord; + +uniform sampler2D u_texture; +uniform float u_alphaTest; + +void main() { + vec4 color = texture2D(u_texture, v_texcoord); + if (color.a < u_alphaTest) { + discard; // не рисовать этот пиксель + } + gl_FragColor = color; +} +``` + +Но дело в том, что это уже нарисует текстуру с прозрачностью и без использования stencil. + +{{{example url="../webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask-example-1.html"}}} + +Если же вы действительно хотите использовать stencil, теперь, когда код отбрасывает некоторые пиксели, всё должно работать, и ваш код был верным. Обратите внимание, что код ниже не очищает stencil, потому что он по умолчанию очищается каждый кадр + +{{{example url="../webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask-example-2.html"}}} + +Также отмечу, что это, вероятно, лучше делать с помощью альфа-смешивания, передавая обе текстуры в один шейдер и передавая ещё одну матрицу или другие uniforms, чтобы применить альфу одной текстуры к другой. Это будет более гибко, так как можно смешивать по всем значениям от 0 до 1, тогда как с помощью stencil можно только маскировать 0 или 1. + +Я не говорю "не используйте stencil", а лишь отмечаю, что бывают ситуации, когда это лучший вариант, и когда — нет. Только вы можете решить, какой способ лучше для вашей задачи. + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + Evan Wrynn + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md b/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md new file mode 100644 index 000000000..09ca2727e --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-use-textures-as-data.md @@ -0,0 +1,183 @@ +Title: Как использовать текстуры как данные +Description: Как использовать текстуры как данные +TOC: Как использовать текстуры как данные + +## Вопрос: + +Я изучал уроки по WebGL, такие как [webglfundamentals](https://webglfundamentals.org/), и столкнулся с проблемой — мне кажется, что мне нужно использовать текстуру, которую я создам, чтобы передавать информацию напрямую во фрагментный шейдер, но у меня не получается правильно индексировать текстуру. + +Цель — передать информацию об источниках света (местоположение и цвет), которая будет учитываться при расчёте цвета фрагмента. В идеале эта информация должна быть динамической как по значению, так и по длине. + +## Воспроизведение +Я создал упрощённую версию проблемы в этом fiddle: [WebGL - Data Texture Testing](https://jsfiddle.net/oclyke/muf0deoL/86/) + +Вот часть кода. + +В **одноразовой инициализации** мы создаём текстуру, заполняем её данными и применяем, как кажется, самые надёжные настройки (без mips, без проблем с упаковкой байтов[?]) +``` + // lookup uniforms + var textureLocation = gl.getUniformLocation(program, "u_texture"); + + // Create a texture. + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // fill texture with 1x3 pixels + const level = 0; + const internalFormat = gl.RGBA; // Я также пробовал gl.LUMINANCE + // но это сложнее отлаживать + const width = 1; + const height = 3; + const border = 0; + const type = gl.UNSIGNED_BYTE; + const data = new Uint8Array([ + // R, G, B, A (не используется) // : индекс 'texel' (?) + 64, 0, 0, 0, // : 0 + 0, 128, 0, 0, // : 1 + 0, 0, 255, 0, // : 2 + ]); + const alignment = 1; // для этой текстуры не обязательно, но + gl.pixelStorei(gl.UNPACK_ALIGNMENT, alignment); // думаю, это не мешает + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, + internalFormat, type, data); + + // set the filtering so we don't need mips and it's not filtered + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); +``` + +В **отрисовке** (которая происходит только один раз, но теоретически может повторяться) мы явно указываем программе использовать нашу текстуру +``` + // Сказать шейдеру использовать текстурный юнит 0 для u_texture + gl.activeTexture(gl.TEXTURE0); // добавил эту и следующую строку для уверенности... + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(textureLocation, 0); +``` + +Наконец, во фрагментном шейдере мы просто пытаемся надёжно использовать один 'texel' для передачи информации. У меня не получается понять, как надёжно получить значения, которые я сохранил в текстуре. +``` +precision mediump float; + +// Текстура. +uniform sampler2D u_texture; + +void main() { + + vec4 sample_00 = texture2D(u_texture, vec2(0, 0)); + // Этот сэмпл обычно правильный. + // Изменение данных для B-канала texel + // индекса 0, как ожидается, добавляет синий + + vec4 sample_01 = texture2D(u_texture, vec2(0, 1)); + vec4 sample_02 = texture2D(u_texture, vec2(0, 2)); + // Эти сэмплы, как я ожидал, должны работать, так как + // ширина текстуры установлена в 1 + // Почему-то 01 и 02 показывают один и тот же цвет + + vec4 sample_10 = texture2D(u_texture, vec2(1, 0)); + vec4 sample_20 = texture2D(u_texture, vec2(2, 0)); + // Эти сэмплы просто для теста — не думаю, + // что они должны работать + + // выбираем, какой сэмпл показать + vec4 sample = sample_00; + gl_FragColor = vec4(sample.x, sample.y, sample.z, 1); +} +``` + +## Вопрос(ы) + +Является ли использование текстуры лучшим способом для этого? Я слышал, что можно передавать массивы векторов, но текстуры, кажется, более распространены. + +Как правильно создавать текстуру? (особенно когда я указываю 'width' и 'height', я должен иметь в виду размеры texel или количество элементов gl.UNSIGNED_BYTE, которые я буду использовать для создания текстуры?? [документация texImage2D](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml)) + +Как правильно индексировать текстуру во фрагментном шейдере, если не использовать 'varying' типы? (т.е. я просто хочу получить значение одного или нескольких конкретных texel — без интерполяции [почти не связано с вершинами]) + +### Другие ресурсы +Я прочитал всё, что мог на эту тему. Вот не полный список: + +* JMI Madison утверждает, что разобрался, но [решение](https://stackoverflow.com/questions/34873832/webgl-fragment-shader-pass-array) утопает в проектном коде +* [webglfundamentals](https://webglfundamentals.org/) почти подходит — [пример с 3x2 data texture](https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html) — но там используется интерполяция и это не совсем мой случай +* Вот кто-то обсуждает [попытку использовать массивы vec3](https://community.khronos.org/t/passing-array-of-vec3-to-fragment-shader/74450) +* И конечно я сравнивал с документацией OpenGL ([texImage2D](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml) и [texture2d](https://thebookofshaders.com/glossary/?search=texture2D)) + +**Edit** Вот ещё ресурс: [Hassles with array access in WebGL, and a couple of workarounds](https://www.john-smith.me/hassles-with-array-access-in-webgl-and-a-couple-of-workarounds.html). Обнадёживает. + +Это меня очень беспокоит. + +Спасибо заранее! + +## Ответ: + +Обращение к отдельным пикселям текстуры в WebGL1 использует такую формулу + +``` +vec2 pixelCoord = vec2(x, y); +vec2 textureDimension = vec2(textureWidth, textureHeight) +vec2 texcoord = (pixelCoord + 0.5) / textureDimensions; +vec4 pixelValue = texture2D(someSamplerUniform, texcoord); +``` + +Потому что текстурные координаты считаются по краям. Если у вас текстура 2x1 + +``` +1.0+-------+-------+ + | | | + | A | B | + | | | +0.0+-------+-------+ + 0.0 0.5 1.0 +``` + +Текстурная координата в центре пикселя A = 0.25, 0.5. В центре пикселя B = 0.75, 0.5 + +Если не использовать формулу выше, а просто pixelCoord / textureDimensions, то вы попадёте между пикселями, и из-за ошибок округления получите не тот пиксель. + +Конечно, если вы используете текстуры для данных, вам также нужно установить фильтрацию `gl.NEAREST`. + +В WebGL2 можно просто использовать `texelFetch` + +``` +ivec2 pixelCoord = ivec2(x, y); +int mipLevel = 0; +vec4 pixelValue = texelFetch(someSamplerUniform, texcoord, mipLevel); +``` + +Рабочий пример использования текстур для данных: [здесь](https://webglfundamentals.org/webgl/lessons/webgl-pulling-vertices.html) + +> Является ли использование текстуры лучшим способом для этого? Я слышал, что можно передавать массивы векторов, но текстуры, кажется, более распространены. + +Для чего? Не совсем понятно, что вы хотите сделать. Каждый пиксель будет иметь свой источник света? + +> Как правильно создавать текстуру? (особенно когда я указываю 'width' и 'height', я должен иметь в виду размеры texel или количество элементов gl.UNSIGNED_BYTE, которые я буду использовать для создания текстуры?? документация texImage2D) + +Делайте так, как проще или как требуется. Например, если у вас 5 данных на объект, я бы поместил каждое значение на отдельную строку текстуры. Тогда можно сделать + +``` +vec4 datum1 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY0)); +vec4 datum2 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY1)); +vec4 datum3 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY2)); +vec4 datum4 = texture2D(dataTexture, vec2(indexTexCoordX, rowTexCoordY3)); +``` + +Где indexTexCoordX и rowTexCoordY0-3 вычисляются по формуле выше. rowTexCoordY0-3 могут быть даже константами. + +У текстур есть ограничения по размеру, так что если данных больше, чем помещается в одном измерении, придётся упаковывать плотнее и делать больше вычислений для извлечения. + +Имейте в виду, что у текстур есть кэш, поэтому желательно, чтобы данные, которые вы извлекаете, были рядом с теми, что вы извлекали до этого. Если вы каждый раз прыгаете по текстуре за следующим значением, производительность упадёт (хотя всё равно может быть быстрее других решений, в зависимости от задачи). + +> Как правильно индексировать текстуру во фрагментном шейдере, если не использовать 'varying' типы? (т.е. я просто хочу получить значение одного или нескольких конкретных texel — без интерполяции [почти не связано с вершинами]) + +Единственные изменяемые входы во фрагментный шейдер — varyings, `gl_FragCoord` (координата пикселя, в который пишется) и `gl_PointCoord`, доступен только при рисовании `POINTS`. Так что нужно использовать один из них, иначе все остальные значения будут одинаковы для всех пикселей. + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + oclyke + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md b/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md new file mode 100644 index 000000000..6cebd840d --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-how-to-use-the-stencil-buffer.md @@ -0,0 +1,93 @@ +Title: Как использовать stencil buffer +Description: Как использовать stencil buffer +TOC: Как использовать stencil buffer + +## Вопрос: + +Как я могу использовать stencil buffer для самой простой программы? +Я прочитал много разных тем об этом, но не нашел подробного руководства. +Я хочу вырезать отверстие на каждой стороне созданного тетраэдра. + +[![enter image description here][1]][1] + +Пожалуйста, объясните мне пошагово, как использовать stencil buffer? + +[Ссылка на мою программу][2] + + + [1]: https://i.stack.imgur.com/yV9oD.png + [2]: https://dropfiles.ru/filesgroup/62503e88028a16b1055f78a7e2b70456.html + +## Ответ: + +Чтобы использовать stencil buffer, вы должны сначала запросить его при создании контекста webgl + + const gl = someCanvasElement.getContext('webgl', {stencil: true}); + + +Затем включите тест трафарета (stencil test) + +``` + gl.enable(gl.STENCIL_TEST); +``` + +Настройте тест так, чтобы он всегда проходил, и установите опорное значение в 1 + +``` + gl.stencilFunc( + gl.ALWAYS, // тест + 1, // опорное значение + 0xFF, // маска + ); +``` + +И задайте операцию так, чтобы мы устанавливали stencil в опорное значение, когда оба теста (stencil и depth) проходят + +``` + gl.stencilOp( + gl.KEEP, // что делать, если stencil тест не прошёл + gl.KEEP, // что делать, если depth тест не прошёл + gl.REPLACE, // что делать, если оба теста прошли + ); +``` + +Теперь рисуем первый внутренний треугольник + +``` +... много настроек для одного треугольника ... + +gl.drawArrays(...) или gl.drawElements(...) +``` + +Затем меняем тест так, чтобы он проходил только если stencil равен нулю + +``` + gl.stencilFunc( + gl.EQUAL, // тест + 0, // опорное значение + 0xFF, // маска + ); + gl.stencilOp( + gl.KEEP, // что делать, если stencil тест не прошёл + gl.KEEP, // что делать, если depth тест не прошёл + gl.KEEP, // что делать, если оба теста прошли + ); + +``` + +и теперь мы можем нарисовать что-то ещё (больший треугольник), и оно будет рисоваться только там, где в stencil buffer стоит 0, то есть везде, кроме области, где был нарисован первый треугольник. + +Пример: + +{{{example url="../webgl-qna-how-to-use-the-stencil-buffer-example-1.html"}}} + + + +
+
Вопрос и процитированные части являются + CC BY-SA 4.0 от + AnatoliyC + из + здесь +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md b/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md new file mode 100644 index 000000000..67aa8b6c4 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-i-get-invalid-type-error-when-calling-readpixels.md @@ -0,0 +1,43 @@ +Title: Ошибка invalid type при вызове readPixels +Description: Ошибка invalid type при вызове readPixels +TOC: Ошибка invalid type при вызове readPixels + +## Вопрос: + + context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.FLOAT, pixels); + +Вот этот код. В консоли появляется ошибка: +**WebGL: INVALID_ENUM: readPixels: invalid type** + +Но вот так всё работает нормально: + + context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.UNSIGNED_BYTE, pixels); + +Float или int вроде как должны поддерживаться, но работает только unsigned_byte. +В интернете нет ресурсов, как правильно применить тип, который, кажется, должен работать. +Везде разные примеры. + +## Ответ: + +FLOAT не гарантирован к поддержке. Единственная комбинация формат/тип, которая гарантированно поддерживается — RGBA/UNSIGNED_BYTE. [См. спецификацию, раздел 4.3.1](https://www.khronos.org/registry/OpenGL/specs/es/2.0/es_full_spec_2.0.pdf) + +Кроме этого, одна другая **зависящая от реализации** комбинация формат/тип может поддерживаться в зависимости от того, что вы читаете. Её можно узнать так: + +``` +const altFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); +const altType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); +``` + +{{{example url="../webgl-qna-i-get-invalid-type-error-when-calling-readpixels-example-1.html"}}} + +Код выше создаёт текстуру RGBA/FLOAT, прикрепляет её к framebuffer, а затем проверяет альтернативную комбинацию формат/тип для чтения. В Chrome это RGBA/UNSIGNED_BYTE, в Firefox — RGBA/FLOAT. Оба варианта валидны, так как альтернативная комбинация **зависит от реализации**. + + + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + Tony Arntsen + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md b/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md new file mode 100644 index 000000000..f51c169f5 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.md @@ -0,0 +1,235 @@ +Title: Показать ночной вид против дневного вида на 3D сфере Земли +Description: Показать ночной вид против дневного вида на 3D сфере Земли +TOC: Показать ночной вид против дневного вида на 3D сфере Земли + +## Вопрос: + +Я использую Three.js как фреймворк для разработки космического симулятора и пытаюсь, но не могу заставить работать ночные огни. + +Симулятор доступен здесь: + +[orbitingeden.com][1] + +и страница, запускающая фрагмент кода ниже, находится здесь: + +[orbitingeden.com/orrery/soloearth.html][2] + +Код для примера страницы здесь. Я даже не знаю, с чего начать. Я пытался рендерить два глобуса на несколько единиц друг от друга, один ближе к солнцу (дневная версия) и один дальше (ночная версия), но есть много проблем, не в последнюю очередь то, что они начинают перекрывать друг друга странными додекаэдрическими способами. Я принял идею tDiffuse2 из этого [оррери][3], но не смог заставить её работать. + + + + + three.js webgl - earth + + + + + + + + + + + [1]: http://orbitingeden.com + [2]: http://orbitingeden.com/orrery/soloearth.html + [3]: http://www.esfandiarmaghsoudi.com/Apps/SolarSystem/ + +## Ответ: + +Если я правильно понимаю ваш вопрос.... + +Я не знаю three.js, но в общем я бы сделал это, имея шейдер, которому передаются и дневная, и ночная текстуры, а затем выбирая одну или другую в шейдере. Например: + + uniform sampler2D dayTexture; + uniform sampler2D nightTexture; + varying vec3 v_surfaceToLight; // assumes this gets passed in from vertex shader + varying vec4 v_normal; // assumes this gets passed in from vertex shader + varying vec2 v_texCoord; // assumes this gets passed in from vertex shader + + void main () { + vec3 normal = normalize(v_normal); + vec3 surfaceToLight = normalize(v_surfaceToLight); + float angle = dot(normal, surfaceToLight); + vec4 dayColor = texture2D(dayTexture, v_texCoords); + vec4 nightColor = texture2D(nightTexture, v_texCoord); + vec4 color = angle < 0.0 ? dayColor : nightColor; + + ... + + gl_FragColor = color * ...; + } + +В основном вы берёте расчёт освещения и вместо использования его для освещения используете его для выбора текстуры. Расчёт освещения обычно использует скалярное произведение между нормалью поверхности и направлением света (солнца) от поверхности. Это даёт вам косинус угла между этими двумя векторами. Косинус идёт от -1 до 1, так что если значение от -1 до 0, это обращено от солнца, если от 0 до +1, это обращено к солнцу. + +Строка + + vec4 color = angle < 0.0 ? dayColor : nightColor; + +выбирает день или ночь. Это будет резкий переход. Вы можете поэкспериментировать с чем-то более размытым, например: + + + // convert from -1 <-> +1 to 0 <-> +1 + float lerp0To1 = angle * 0.5 + 0.5; + + // mix between night and day + vec4 color = mix(nightColor, dayColor, lerp0to1); + + +Это даст вам 100% день в точке, прямо обращённой к солнцу, и 100% ночь в точке, прямо противоположной солнцу, и смесь между ними. Вероятно, не то, что вы хотите, но вы можете поиграть с числами. Например: + + // sharpen the mix + angle = clamp(angle * 10.0, -1.0, 1.0); + + // convert from -1 <-> +1 to 0 <-> +1 + float lerp0To1 = angle * 0.5 + 0.5; + + // mix between night and day + vec4 color = mix(nightColor, dayColor, lerp0to1); + + +Надеюсь, это имело смысл. + +---- + +Так что я потратил немного времени на создание примера Three.js, отчасти чтобы изучить Three.js. Пример здесь. + +{{{example url="../webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere-example-1.html"}}} + +Шейдер, который я использовал, это: + + uniform sampler2D dayTexture; + uniform sampler2D nightTexture; + + uniform vec3 sunDirection; + + varying vec2 vUv; + varying vec3 vNormal; + + void main( void ) { + vec3 dayColor = texture2D( dayTexture, vUv ).rgb; + vec3 nightColor = texture2D( nightTexture, vUv ).rgb; + + // compute cosine sun to normal so -1 is away from sun and +1 is toward sun. + float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection); + + // sharpen the edge beween the transition + cosineAngleSunToNormal = clamp( cosineAngleSunToNormal * 10.0, -1.0, 1.0); + + // convert to 0 to 1 for mixing + float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5; + + // Select day or night texture based on mixAmount. + vec3 color = mix( nightColor, dayColor, mixAmount ); + + gl_FragColor = vec4( color, 1.0 ); + + // comment in the next line to see the mixAmount + //gl_FragColor = vec4( mixAmount, mixAmount, mixAmount, 1.0 ); + } + +Большая разница от предыдущего в том, что поскольку солнце обычно считается направленным источником света, поскольку оно так далеко, то всё, что вам нужно — это его направление. Другими словами, в какую сторону оно указывает относительно земли. + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + Orbiting Eden + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md b/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md new file mode 100644 index 000000000..dd68944cb --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-the-fastest-way-to-draw-many-circles.md @@ -0,0 +1,76 @@ +Title: Самый быстрый способ нарисовать много кругов +Description: Самый быстрый способ нарисовать много кругов +TOC: Самый быстрый способ нарисовать много кругов + +## Вопрос: + +Сейчас я рисую тысячи кругов, инстанцируя [геометрию круга](https://threejs.org/docs/#api/en/geometries/CircleBufferGeometry) (много треугольников). + +[![enter image description here][1]][1] + +Альтернативно, я мог бы просто инстанцировать quad (2 треугольника), а круг "вырезать" во фрагментном шейдере с помощью distance-функции и `discard`. + +[![enter image description here][2]][2] + +Какой подход будет быстрее? — дороже ли рисовать много треугольников, чем вычисления во фрагментном шейдере? + + [1]: https://i.stack.imgur.com/CZhOa.png + [2]: https://i.stack.imgur.com/BG4p5.png + +## Ответ: + +Самый быстрый способ может зависеть от GPU и множества других факторов: как вы рисуете круги (2D, 3D), используете ли смешивание (blending), z-buffer и т.д. Но в целом — меньше треугольников быстрее, чем больше, и меньше пикселей быстрее, чем больше. Так что... всё, что мы можем — это попробовать. + +Сначала просто нарисуем текстурированные квадраты без blending. У меня на GPU в этом 300x150 canvas с инстансингом получается 20-30 тысяч quad'ов на 60fps. + +{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-1.html"}}} + +И такая же производительность при 60fps, если использовать повторяющуюся геометрию вместо инстансинга. Это удивительно, потому что 7-8 лет назад повторяющаяся геометрия была на 20-30% быстрее. Почему — не знаю: лучше GPU, драйвер или что-то ещё. + +{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-2.html"}}} + +Дальше — текстуры или вычисление круга во фрагментном шейдере. + +{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-3.html"}}} + +Разницы не заметил. Пробую вашу функцию круга: + +{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-4.html"}}} + +Опять разницы не заметил. Замечу: как уже говорил, результаты WebGL очень нестабильны. Первый тест — 28k на 60fps, второй — 23k. Ожидал, что второй будет быстрее, но потом снова первый — 23k, последний — 29k, потом предыдущий — 29k. Короче, тестировать производительность в WebGL почти невозможно. Слишком много факторов, всё многопроцессное, получить стабильные результаты невозможно. + +Можно попробовать discard: + +{{{example url="../webgl-qna-the-fastest-way-to-draw-many-circles-example-5.html"}}} + +Судя по ощущениям, discard медленнее. Насколько помню, discard медленный, потому что без него GPU знает заранее, что обновит z-buffer, а с discard — только после выполнения шейдера, и это мешает оптимизациям. + +На этом остановлюсь, потому что слишком много комбинаций для тестов. + +Можно попробовать включить blending. Blending обычно медленнее, потому что нужно смешивать (читать фон), но медленнее ли, чем discard — не знаю. + +Включён ли depth test? Если да, порядок отрисовки важен. + +Можно попробовать не-quads, а, например, шестиугольники или восьмиугольники — тогда меньше пикселей попадёт во фрагментный шейдер. Думаю, разницу увидите только на больших кругах. Например, quad 100x100 — это 10k пикселей, идеальный круг — pi*r^2 ≈ 7853, то есть на 21% меньше. Шестиугольник — ~8740 пикселей, на 11% меньше. Восьмиугольник — где-то между. Рисовать на 11-21% меньше пикселей — обычно плюс, но для шестиугольника будет в 3 раза больше треугольников, для восьмиугольника — в 4 раза. Всё надо тестировать. + +Это ещё раз показывает, что для больших кругов на большом canvas относительные результаты будут другими: больше пикселей на круг, больше времени на пиксели, меньше — на вершины и/или переключения GPU. + +## Обновление + +Тесты в Chrome и Firefox: в Chrome на той же машине 60-66k во всех случаях. Почему разница такая большая — не знаю, ведь WebGL почти ничего не делает. Все 4 теста — по одному draw call на кадр. Но, по крайней мере, на 2019-10 Chrome в этом случае в 2 раза быстрее Firefox. + +Есть идея: у меня ноутбук с двумя GPU. При создании контекста можно указать, что вы хотите, через атрибут `powerPreference`: + + const gl = document.createContext('webgl', { + powerPreference: 'high-performance', + }); + +Варианты: 'default', 'low-power', 'high-performance'. 'default' — "пусть браузер решает", но в итоге всё равно решает браузер. В любом случае, у меня в Firefox это ничего не меняет. + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + kindoflike + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md b/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md new file mode 100644 index 000000000..2bae37212 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-webgl-2d-tilemaps.md @@ -0,0 +1,42 @@ +Title: WebGL 2D тайлмапы +Description: WebGL 2D тайлмапы +TOC: WebGL 2D тайлмапы + +## Вопрос: + +Я создаю простую 2D веб-игру с типичной тайл-картой и спрайтами. + +Особенность в том, что мне нужны плавные элементы управления камерой — как перемещение, так и масштабирование (зум). + +Я пробовал и Canvas 2D API, и WebGL, но в обоих случаях не могу избежать артефактов сетки (bleeding grid line artifacts), при этом поддерживая правильное масштабирование. + +Если это важно: все мои тайлы размером 1, масштабируются до нужного размера, все координаты — целые числа, и я использую текстуру-атлас. + +Вот пример картинки с моим WebGL-кодом, где тонкие красные/белые линии нежелательны: +[![enter image description here][1]][1] + + [1]: https://i.stack.imgur.com/ZziiR.png + +Я помню, как писал спрайтовые тайлмапы годами назад на desktop GL, иронично используя похожий код (более или менее эквивалентный тому, что можно сделать с WebGL 2), и там никогда не было таких проблем. + +Сейчас думаю попробовать DOM-элементы, но боюсь, что это не будет выглядеть или работать плавно. + +## Ответ: + +Одно из решений — рисовать тайлы во фрагментном шейдере. + +У вас есть карта, скажем, `Uint32Array`. Разбиваете её на единицы по 4 байта каждая. Первые 2 байта — ID тайла, последний байт — флаги. + +Когда вы проходите по quad'у для каждого пикселя, вы смотрите в текстуру тайлмапы, какой это тайл, затем используете это для вычисления UV-координат, чтобы получить пиксели этого тайла из текстуры тайлов. Если у вашей текстуры тайлов установлена выборка gl.NEAREST, то bleeding никогда не будет. + +Заметьте, что в отличие от традиционных тайлмапов, ID каждого тайла — это X,Y-координата тайла в текстуре тайлов. Другими словами, если ваша текстура тайлов имеет 16x8 тайлов, и вы хотите показать тайл на позиции 7 по горизонтали и 4 по вертикали, то ID этого тайла — 7,4 (первый байт 7, второй байт 4), тогда как в традиционной CPU-системе ID тайла был бы, вероятно, 4*16+7 или 71 (71-й тайл). Можно добавить код в шейдер для более традиционной индексации, но поскольку шейдер всё равно должен конвертировать ID в 2D-координаты текстуры, проще использовать 2D-ID. + +{{{example url="../webgl-qna-webgl-2d-tilemaps-example-1.html"}}} + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + user2503048 + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md b/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md new file mode 100644 index 000000000..4e2c9dcef --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-webgl-droste-effect.md @@ -0,0 +1,90 @@ +Title: Эффект Дросте в WebGL +Description: Эффект Дросте в WebGL +TOC: Эффект Дросте в WebGL + +## Вопрос: + +Я пытаюсь использовать WebGL для создания [эффекта Дросте](https://en.wikipedia.org/wiki/Droste_effect) на гранях куба. В viewport'е один меш — куб, и все его грани используют одну текстуру. Для создания эффекта Дросте я обновляю текстуру на каждом кадре, делая снимок `canvas`, в WebGL-контекст которого рисую, что со временем даёт эффект Дросте, так как снимок содержит всё больше и больше вложенных прошлых кадров. + +Демо того, что у меня сейчас работает, здесь: + +https://tomashubelbauer.github.io/webgl-op-1/?cubeTextured + +Код выглядит так: + +``` +// Set up fragment and vertex shader and attach them to a program, link the program +// Create a vertex buffer, an index buffer and a texture coordinate buffer +// Tesselate the cube's vertices and fill in the index and texture coordinate buffers +const textureCanvas = document.createElement('canvas'); +textureCanvas.width = 256; +textureCanvas.height = 256; +const textureContext = textureCanvas.getContext('2d'); + +// In every `requestAnimationFrame`: +textureContext.drawImage(context.canvas, 0, 0); +const texture = context.createTexture(); +context.bindTexture(context.TEXTURE_2D, texture); +context.texImage2D(context.TEXTURE_2D, 0, context.RGBA, context.RGBA, context.UNSIGNED_BYTE, textureCanvas); +context.generateMipmap(context.TEXTURE_2D); +// Clear the viewport completely (depth and color buffers) +// Set up attribute and uniform values, the projection and model view matrices +context.activeTexture(context.TEXTURE0); +context.bindTexture(context.TEXTURE_2D, texture); +context.uniform1i(fragmentShaderTextureSamplerUniformLocation, 0); +context.drawElements(context.TRIANGLES, 36, context.UNSIGNED_SHORT, 0) +``` + +Это основная часть. Есть отдельный canvas от WebGL, на который рисуется WebGL canvas перед каждым WebGL кадром, и этот canvas используется для создания текстуры для данного кадра, которая применяется к граням куба согласно буферу координат текстуры и uniform'у сэмплера текстуры, переданному во фрагментный шейдер, который просто использует `gl_FragColor = texture2D(textureSampler, textureCoordinate)`, как и ожидается. + +Но это очень медленно (30 FPS на этом простом демо с одним кубом, тогда как все мои другие демо с порядком больше треугольников всё ещё держат 60 FPS `requestAnimationFrame`). + +Также странно делать это "вне" WebGL, используя внешний canvas, когда кажется, что это должно быть достижимо только с помощью WebGL. + +Я знаю, что WebGL держит два буфера — один для активного кадра и back buffer для недавно нарисованного кадра, и они меняются местами с каждым кадром для мгновенного обновления экрана. Можно ли получить доступ к этому back buffer и использовать его как текстуру? Можете ли вы предоставить пример кода, как это сделать? + +## Ответ: + +Из [этой статьи](https://webglfundamentals.org/webgl/lessons/webgl-render-to-texture.html) + +Обычный способ — рендерить в текстуру, прикрепив её к framebuffer. + +``` +const fb = gl.createFramebuffer(); +gl.bindFramebuffer(gl.FRAMEBUFFER, fb); +gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0 /* level */) +``` + +Теперь для рендеринга в текстуру: + +``` +gl.bindFramebuffer(gl.FRAMEBUFFER, fb); +gl.viewport(0, 0, textureWidth, textureHeight); +``` + +Для рендеринга в canvas: + +``` +gl.bindFramebuffer(gl.FRAMEBUFFER, null); +gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); +``` + +Для того, что вы хотите, нужны 2 текстуры, поскольку нельзя одновременно читать из и писать в одну текстуру, так что вы рисуете: + +* Рисуете изображение в TextureA +* Рисуете предыдущий кадр (TextureB) в TextureA +* Рисуете куб с TextureA в TextureB +* Рисуете TextureB в Canvas + +{{{example url="../webgl-qna-webgl-droste-effect-example-1.html"}}} + +Что касается canvas и его 2 буферов — нет, нельзя напрямую использовать их как текстуры. Можно вызвать `gl.copyTexImage2D` или `gl.copyTexSubImage2D` для копирования части canvas в текстуру, так что это другое решение. Оно менее гибкое и, думаю, медленнее, чем метод framebuffer. + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + Tomáš Hübelbauer + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md b/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md new file mode 100644 index 000000000..d88c9508e --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-what-is-the-local-origin-of-a-3d-model-.md @@ -0,0 +1,565 @@ +Title: Что такое локальное начало координат 3D модели? +Description: Что такое локальное начало координат 3D модели? +TOC: Что такое локальное начало координат 3D модели? + +## Вопрос: + +В этом коде я не могу понять, почему вращение не происходит по круговой траектории. Это может быть базовая логика, но я не понимаю, почему траектория такая случайная. + +Я вращаю камеру по орбитальному движению, но она не следует за ним. + +Насколько я понимаю, я создал орбитальную камеру, и обратная к ней — это матрица вида. Значит, матрица вида будет трансформировать мировое пространство для этого результата. Есть ли ошибка в моём мышлении? + + + + + + "use strict"; + + const vertexShader = `#version 300 es + + in vec4 a_position; + in vec4 a_color; + + out vec4 v_color; + + uniform mat4 u_matrix; + + void main(){ + gl_Position = u_matrix*a_position; + v_color = a_color; + } + `; + + + const fragShader = `#version 300 es + + precision highp float; + + in vec4 v_color; + out vec4 frag_color; + + void main(){ + frag_color = v_color; + } + `; + + var cameraAngleDegree = 0; + var cameraAngle = 0; + const radius = 100; + var increment = 1; + var numFs = 5; + function main() { + + var canvas = document.querySelector("#canvas"); + var gl = canvas.getContext("webgl2"); + if (!gl) { + return; + } + requestAnimationFrame(function() { + init(gl); + }); + } + + function init(gl) { + + + const program = webglUtils.createProgramFromSources(gl, [vertexShader, fragShader]); + + const apositionLoc = gl.getAttribLocation(program, 'a_position'); + const acolorLoc = gl.getAttribLocation(program, 'a_color'); + const umatrixLoc = gl.getUniformLocation(program, 'u_matrix'); + + let vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + + let positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + setGeometry(gl); + gl.enableVertexAttribArray(apositionLoc); + + let size = 3; + let type = gl.FLOAT; + let normalize = false; + let stride = 0; + let offset = 0; + gl.vertexAttribPointer(apositionLoc, size, type, normalize, stride, offset); + + let colorBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + setColor(gl); + gl.enableVertexAttribArray(acolorLoc); + + size = 3; + type = gl.UNSIGNED_BYTE; + normalize = true; + stride = 0; + offset = 0; + gl.vertexAttribPointer(acolorLoc, size, type, normalize, stride, offset); + + let fov = degreeToRadian(60); + cameraAngle = degreeToRadian(cameraAngleDegree); + + function degreeToRadian(deg) { + return deg * Math.PI / 180; + } + + function radToDegree(rad) { + return rad * (180) / Math.PI; + } + + drawScene(); + + // webglLessonsUI.setupSlider("#cameraAngle", { value: radToDegree(cameraAngle), slide: updateCameraAngle, min: -360, max: 360 }); + + // function updateCameraAngle(event, ui) { + // cameraAngle = degreeToRadian(ui.value); + // drawScene(); + // } + + + function drawScene() { + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + gl.enable(gl.CULL_FACE); + + gl.enable(gl.DEPTH_TEST); + + gl.useProgram(program); + + let aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + + let projection = m4.perspective(fov, aspect, 1, 1000); + + const fPosition = [radius, 0, 0]; + + cameraAngleDegree += increment; + + cameraAngle =degreeToRadian(cameraAngleDegree); + + let camera = m4.yRotation(cameraAngle); + camera = m4.translate(camera, 0, 100, 300); + + let cameraPosition = [camera[12], camera[13], camera[14]]; + + // let up = [0, 1, 0]; + + // camera = m4.lookAt(cameraPosition, fPosition, up); + + let viewMatrix = m4.inverse(camera); + + let viewProjection = m4.multiply(projection, viewMatrix); + + for (var ii = 0; ii < numFs; ++ii) { + var angle = ii * Math.PI * 2 / numFs; + + var x = Math.cos(angle) * radius - 50; + var z = Math.sin(angle) * radius - 15; + var matrix = m4.translate(viewProjection, x, 0, z); + + // Set the matrix. + gl.uniformMatrix4fv(umatrixLoc, false, matrix); + + // Draw the geometry. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 16 * 6; + gl.drawArrays(primitiveType, offset, count); + } + // gl.uniformMatrix4fv(umatrixLoc, false, viewProjection); + + // var primitives = gl.TRIANGLES; + // var count = 16 * 6; + // var offset = 0; + // gl.drawArrays(primitives, offset, count); + + // } + + requestAnimationFrame(function() { + init(gl) + }); + + } + } + + function setGeometry(gl) { + + let positions = new Float32Array([ + + 0, 0, 0, + 0, 150, 0, + 30, 0, 0, + 0, 150, 0, + 30, 150, 0, + 30, 0, 0, + + // top rung front + 30, 0, 0, + 30, 30, 0, + 100, 0, 0, + 30, 30, 0, + 100, 30, 0, + 100, 0, 0, + + // middle rung front + 30, 60, 0, + 30, 90, 0, + 67, 60, 0, + 30, 90, 0, + 67, 90, 0, + 67, 60, 0, + + // left column back + 0, 0, 30, + 30, 0, 30, + 0, 150, 30, + 0, 150, 30, + 30, 0, 30, + 30, 150, 30, + + // top rung back + 30, 0, 30, + 100, 0, 30, + 30, 30, 30, + 30, 30, 30, + 100, 0, 30, + 100, 30, 30, + + // middle rung back + 30, 60, 30, + 67, 60, 30, + 30, 90, 30, + 30, 90, 30, + 67, 60, 30, + 67, 90, 30, + + // top + 0, 0, 0, + 100, 0, 0, + 100, 0, 30, + 0, 0, 0, + 100, 0, 30, + 0, 0, 30, + + // top rung right + 100, 0, 0, + 100, 30, 0, + 100, 30, 30, + 100, 0, 0, + 100, 30, 30, + 100, 0, 30, + + // under top rung + 30, 30, 0, + 30, 30, 30, + 100, 30, 30, + 30, 30, 0, + 100, 30, 30, + 100, 30, 0, + + // between top rung and middle + 30, 30, 0, + 30, 60, 30, + 30, 30, 30, + 30, 30, 0, + 30, 60, 0, + 30, 60, 30, + + // top of middle rung + 30, 60, 0, + 67, 60, 30, + 30, 60, 30, + 30, 60, 0, + 67, 60, 0, + 67, 60, 30, + + // right of middle rung + 67, 60, 0, + 67, 90, 30, + 67, 60, 30, + 67, 60, 0, + 67, 90, 0, + 67, 90, 30, + + // bottom of middle rung. + 30, 90, 0, + 30, 90, 30, + 67, 90, 30, + 30, 90, 0, + 67, 90, 30, + 67, 90, 0, + + // right of bottom + 30, 90, 0, + 30, 150, 30, + 30, 90, 30, + 30, 90, 0, + 30, 150, 0, + 30, 150, 30, + + // bottom + 0, 150, 0, + 0, 150, 30, + 30, 150, 30, + 0, 150, 0, + 30, 150, 30, + 30, 150, 0, + + // left side + 0, 0, 0, + 0, 0, 30, + 0, 150, 30, + 0, 0, 0, + 0, 150, 30, + 0, 150, 0, + + ]); + + gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW) + } + + function setColor(gl) { + gl.bufferData( + gl.ARRAY_BUFFER, + new Uint8Array([ + // left column front + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + + // top rung front + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + + // middle rung front + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + 200, 70, 120, + + // left column back + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + + // top rung back + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + + // middle rung back + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + 80, 70, 200, + + // top + 70, 200, 210, + 70, 200, 210, + 70, 200, 210, + 70, 200, 210, + 70, 200, 210, + 70, 200, 210, + + // top rung right + 200, 200, 70, + 200, 200, 70, + 200, 200, 70, + 200, 200, 70, + 200, 200, 70, + 200, 200, 70, + + // under top rung + 210, 100, 70, + 210, 100, 70, + 210, 100, 70, + 210, 100, 70, + 210, 100, 70, + 210, 100, 70, + + // between top rung and middle + 210, 160, 70, + 210, 160, 70, + 210, 160, 70, + 210, 160, 70, + 210, 160, 70, + 210, 160, 70, + + // top of middle rung + 70, 180, 210, + 70, 180, 210, + 70, 180, 210, + 70, 180, 210, + 70, 180, 210, + 70, 180, 210, + + // right of middle rung + 100, 70, 210, + 100, 70, 210, + 100, 70, 210, + 100, 70, 210, + 100, 70, 210, + 100, 70, 210, + + // bottom of middle rung. + 76, 210, 100, + 76, 210, 100, + 76, 210, 100, + 76, 210, 100, + 76, 210, 100, + 76, 210, 100, + + // right of bottom + 140, 210, 80, + 140, 210, 80, + 140, 210, 80, + 140, 210, 80, + 140, 210, 80, + 140, 210, 80, + + // bottom + 90, 130, 110, + 90, 130, 110, + 90, 130, 110, + 90, 130, 110, + 90, 130, 110, + 90, 130, 110, + + // left side + 160, 160, 220, + 160, 160, 220, + 160, 160, 220, + 160, 160, 220, + 160, 160, 220, + 160, 160, 220, + ]), + gl.STATIC_DRAW); + } + + main(); + + + + + + + + Traingle Webgl 2 + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + +## Ответ: + +Если я правильно понимаю ваш вопрос, проблема в том, что кажется, будто камера то приближается к F, то удаляется от них. + +Проблема в том, что данные вершин для F построены так, что верхний левый передний угол находится в 0,0,0, оттуда они идут +X на 100 единиц (ширина 100 единиц), +Y на 150 единиц (высота 150 единиц), и +Z на 30 единиц (глубина 30 единиц). + +Поэтому когда вы рисуете их вокруг круга радиусом 100 единиц, их начало координат — это та часть, которую вы позиционируете, и получается это: + +[![enter image description here][1]][1] + +Изображение сверху, так что F — это просто прямоугольники. Зелёный круг — это локальное начало координат каждой F, её локальные 0,0,0. Другие вершины F относительно этого локального начала, поэтому они ближе к внешнему кругу (орбите камеры) с одной стороны и дальше с другой. + +Можно исправить, сдвинув F на -50 по X и -15 по Z. Другими словами: + +``` + var angle = ii * Math.PI * 2 / numFs; + + var x = Math.cos(angle) * radius - 50; + var z = Math.sin(angle) * radius - 15; +``` + +Что даёт такую ситуацию: + +[![enter image description here][2]][2] + +Локальное начало координат каждой F больше не на круге. + +Также можно исправить, центрировав данные вершин F — пройти по всем вершинам и вычесть 50 из X и 15 из Z. Это даст такую ситуацию: + +[![enter image description here][3]][3] + +Теперь начало координат каждой F центрировано, и её локальное начало на круге. + +Ещё один способ исправить — вычислить экстенты группы F, вычислить центр экстентов, переместить центр орбиты камеры туда, что даст такую ситуацию: + +[![enter image description here][4]][4] + + [1]: https://i.stack.imgur.com/bW9Xo.png + [2]: https://i.stack.imgur.com/CejYi.png + [3]: https://i.stack.imgur.com/MiQ6Z.png + [4]: https://i.stack.imgur.com/oysVQ.png + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + pravin poudel + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md b/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md new file mode 100644 index 000000000..10a1b388b --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md @@ -0,0 +1,65 @@ +Title: Когда выбирать highp, mediump, lowp в шейдерах +Description: Когда выбирать highp, mediump, lowp в шейдерах +TOC: Когда выбирать highp, mediump, lowp в шейдерах + +## Вопрос: + +Какие лучшие практики для них? Есть ли разница в производительности? + +## Ответ: + +> Какие лучшие практики для них? + +В основном это важно только на мобильных устройствах. Спецификация говорит, что реализация может всегда использовать более высокую точность, поэтому на десктопе и вершинный шейдер, и фрагментный шейдер всегда работают в highp. (Я не знаю десктопных GPU, для которых это не так) + +Из [спецификации](https://www.khronos.org/files/opengles_shading_language.pdf) раздел 4.5.2: + +> ## 4.5.2 Precision Qualifiers +> +> ... +> +> Precision qualifiers declare a minimum range and precision that the underlying implementation must use +> when storing these variables. Implementations may use greater range and precision than requested, but +> not less. + +Для мобильных устройств и планшетов есть несколько ответов. Нет лучшего. Это зависит от вас: + +1. используйте самую низкую точность, которую можете, но которая всё ещё делает то, что вам нужно. + +2. используйте highp и игнорируйте проблемы производительности и старые телефоны, где это не работает + +3. используйте mediump и игнорируйте баги (см. ниже) + +4. проверьте, поддерживает ли устройство пользователя highp, если нет — используйте разные шейдеры с меньшим количеством функций. + +WebGL по умолчанию использует highp для вершинных шейдеров, а фрагментные шейдеры не имеют значения по умолчанию, и вы должны указать одно. Более того, highp во фрагментном шейдере — это опциональная функция, и некоторые мобильные GPU её не поддерживают. Я не знаю, какой это процент в 2019 году. Насколько я знаю, большинство или даже все телефоны, выпущенные в 2019 году, поддерживают highp, но старые телефоны (2011, 2012, 2013) не поддерживают. + +Из спецификации: + +> The vertex language requires any uses of `lowp`, `mediump` and `highp` to compile and link without error. +> The fragment language requires any uses of `lowp` and `mediump` to compile without error. **Support for +> `highp` is optional**. + +Примеры мест, где обычно нужен highp. Точечные источники света с затенением по Фонгу обычно нуждаются в highp. Так что, например, вы можете использовать только направленные источники света на системе, которая не поддерживает highp, ИЛИ вы можете использовать только направленные источники света на мобильных для производительности. + +> Есть ли разница в производительности? + +Да, но как сказано выше, реализация может использовать более высокую точность. Так что если вы используете mediump на десктопном GPU, вы не увидите разницы в производительности, поскольку он действительно всегда использует highp. На мобильных вы увидите разницу в производительности, по крайней мере в 2019 году. Вы также можете увидеть, где вашим шейдерам действительно нужен highp. + +Вот шейдер Фонга, настроенный на использование mediump. На десктопе, поскольку mediump на самом деле highp, он работает: + +![](https://user-images.githubusercontent.com/234804/43352753-6ed6b9f8-9263-11e8-9716-3819a8c92095.png) + +На мобильных, где mediump действительно mediump, он ломается: + +![](https://user-images.githubusercontent.com/234804/43352759-7f9d1656-9263-11e8-8487-aa57d6092ff1.png) + +Пример, где mediump был бы хорош, по крайней мере во фрагментном шейдере — это большинство 2D игр. + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 4.0 у + WayneNWayne + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md b/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md new file mode 100644 index 000000000..514a1c04c --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-working-around-gl_pointsize-limitations-webgl.md @@ -0,0 +1,121 @@ +Title: Обход ограничений gl_PointSize в WebGL +Description: Как обойти ограничения gl_PointSize в WebGL +TOC: Обход ограничений gl_PointSize в WebGL + +## Вопрос: + +Я использую three.js для создания интерактивной визуализации данных. В этой визуализации рендерится 68000 узлов, каждый из которых имеет свой размер и цвет. + +Сначала я пытался делать это через рендеринг мешей, но это оказалось слишком дорого по производительности. Сейчас я использую систему частиц three.js, где каждая точка — это узел визуализации. + +Я могу управлять цветом и размером точки, но только до определённого предела. На моей видеокарте максимальный размер точки gl равен 63. Когда я увеличиваю масштаб визуализации, точки становятся больше — до определённого момента, а затем остаются 63 пикселя. + +Я использую собственные vertex и fragment шейдеры: + +vertex shader: + + attribute float size; + attribute vec3 ca; + varying vec3 vColor; + + void main() { + vColor = ca; + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + gl_PointSize = size * ( 300.0 / length( mvPosition.xyz ) ); + gl_Position = projectionMatrix * mvPosition; + } + +fragment shader: + + uniform vec3 color; + uniform sampler2D texture; + + varying vec3 vColor; + + void main() { + gl_FragColor = vec4( color * vColor, 1.0 ); + gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord ); + } + +Эти шейдеры почти дословно скопированы из одного из примеров three.js. + +Я совсем новичок в GLSL, но ищу способ рисовать точки больше 63 пикселей. Могу ли я, например, рисовать меш для точек больше определённого размера, а для остальных использовать gl_point? Есть ли другие способы обойти это ограничение и рисовать точки больше 63 пикселей? + +## Ответ: + +Вы можете сделать свою собственную систему точек, используя массивы unit quad'ов + центральную точку, а затем масштабировать их по размеру в GLSL. + +То есть, у вас будет 2 буфера. Один буфер — это просто 2D unitQuad, повторённый столько раз, сколько точек вы хотите нарисовать. + + var unitQuads = new Float32Array([ + -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, + -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, + -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, + -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, + -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, + ]); + +Второй буфер — это ваши точки, но позиции каждой точки нужно повторить 4 раза: + + var points = new Float32Array([ + p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, p1.x, p1.y, p1.z, + p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, p2.x, p2.y, p2.z, + p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, p3.x, p3.y, p3.z, + p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, p4.x, p4.y, p4.z, + p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, p5.x, p5.y, p5.z, + ]); + +Настройте ваши буферы и атрибуты: + + var buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, unitQuads, gl.STATIC_DRAW); + gl.enableVertexAttribArray(unitQuadLoc); + gl.vertexAttribPointer(unitQuadLoc, 2, gl.FLOAT, false, 0, 0); + + var buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); + gl.enableVertexAttribArray(pointLoc); + gl.vertexAttribPointer(pointLoc, 3, gl.FLOAT, false, 0, 0); + +В вашем GLSL-шейдере вычисляйте нужный gl_PointSize, затем умножайте unitQuad на этот размер в view space или screen space. Screen space будет вести себя как обычный gl_Point, но часто хочется, чтобы точки масштабировались в 3D, как обычные объекты — тогда используйте view space. + + attribute vec2 a_unitQuad; + attribute vec4 a_position; + uniform mat4 u_view; + uniform mat4 u_viewProjection; + + void main() { + float fake_gl_pointsize = 150; + + // Получаем xAxis и yAxis во view space + // это unit-векторы, то есть направления, перпендикулярные взгляду + vec3 x_axis = view[0].xyz; + vec3 y_axis = view[1].xyz; + + // умножаем их на нужный размер + x_axis *= fake_gl_pointsize; + y_axis *= fake_gl_pointsize; + + // умножаем на unitQuad, чтобы получить quad вокруг центра + vec3 local_point = vec3(x_axis * a_unitQuad.x + y_axis * a_unitQuad.y); + + // добавляем позицию, где хотим разместить quad + local_point += a_position; + + // обычная математика для шейдера + gl_Position = u_viewProjection * local_point; + } + +Возможно, это звучит сложно, но есть рабочий пример [здесь][1] + + [1]: https://www.khronos.org/registry/webgl/sdk/demos/google/particles/ + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + Thomi + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md b/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md new file mode 100644 index 000000000..4cee2984d --- /dev/null +++ b/webgl/lessons/ru/webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.md @@ -0,0 +1,60 @@ +Title: Приближение и остановка у объекта в сцене WebGL +Description: Как реализовать приближение к объекту и остановку перед ним в сцене WebGL +TOC: Приближение и остановка у объекта в сцене WebGL + +## Вопрос: + +Мы создали WebGL-приложение, которое отображает сцену с несколькими объектами. Всю сцену можно вращать в разных направлениях. Требуется, чтобы пользователь мог приближаться к объекту, но НЕ проходить сквозь него. Я знаю, что такую функциональность можно реализовать с помощью фреймворков вроде Three.js и SceneJs. К сожалению, наше приложение не использует фреймворки. Можно ли реализовать описанное приближение только средствами WebGL? Примечание: object picking нам не подходит, так как пользователь не обязан выбирать объект в сцене. Спасибо за помощь. + +## Ответ: + +Навскидку: + +Во-первых, нужно знать размер каждого объекта в мировом пространстве. Например, если один объект размером 10 единиц, а другой — 100, вероятно, вы захотите находиться на разном расстоянии от них. Под мировым пространством я также имею в виду, что если вы масштабируете объект 10 на 9, то в мире он будет 90 единиц, и снова потребуется другое расстояние. + +Обычно размер объекта в локальном пространстве вычисляют по экстентам его вершин. Просто проходите по всем вершинам и отслеживайте минимальные и максимальные значения по x, y и z. Хотите ли вы брать максимальное значение от центра или вычислять реальный центр — решайте сами. + +Зная размер, можно вычислить, на каком расстоянии нужно быть, чтобы видеть весь объект. Для стандартной перспективной матрицы можно просто посчитать это в обратную сторону. Если объект 10 единиц, нужно уместить 10 единиц в фрустуме. Обычно берут чуть больше, например, 14 (size * 1.4), чтобы был запас вокруг объекта. + +![enter image description here][1] + +Известны `halfFovy`, `halfSizeToFitOnScreen`, нужно вычислить `distance`: + + sohcahtoa + tangent = opposite / adjacent + opposite = halfsizeToFitOnScreen + adjacent = distance + tangent = Math.tan(halfFovY) + +Следовательно: + + tangent = sizeToFitOnScreen / distance + tangent * distance = sizeToFitOnScreen + distance = sizeToFitOnScreen / tangent + distance = sizeToFitOnScreen / Math.tan(halfFovY) + +Теперь мы знаем, что камере нужно быть на расстоянии `distance` от объекта. На этом расстоянии вокруг объекта есть целая сфера. Где именно на этой сфере — решать вам. Обычно берут текущее положение камеры и вычисляют направление от объекта к камере: + + direction = normalize(cameraPos - objectPos) + +Теперь можно вычислить точку на расстоянии `distance` в этом направлении: + + desiredCameraPosition = direction * distance + +Теперь либо ставьте камеру туда с помощью lookAt: + + matrix = lookAt(desiredCameraPosition, objectPosition, up) + +Либо плавно перемещайте камеру (lerp) между текущей позицией и новой: + +{{{example url="../webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl-example-1.html"}}} + + [1]: http://i.stack.imgur.com/0axue.png + +
+
Вопрос и цитируемые части взяты по лицензии CC BY-SA 3.0 у + jfc615 + с сайта + stackoverflow +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-qna.md b/webgl/lessons/ru/webgl-qna.md new file mode 100644 index 000000000..bc9e38917 --- /dev/null +++ b/webgl/lessons/ru/webgl-qna.md @@ -0,0 +1,123 @@ +Title: Вопросы и ответы +Description: Случайные вопросы и ответы +TOC: Вопросы и ответы + +Коллекция ссылок на случайные вопросы и/или ответы о темах, связанных с WebGL + +* [Эффект Дросте в WebGL](webgl-qna-webgl-droste-effect.html) +* [Обход ограничений gl_PointSize в WebGL](webgl-qna-working-around-gl_pointsize-limitations-webgl.html) +* [Как симулировать 3D текстуру в WebGL](webgl-qna-how-to-simulate-a-3d-texture-in-webgl.html) +* [Как рендерить изображения большого масштаба, такие как 32000x32000](webgl-qna-how-to-render-large-scale-images-like-32000x32000.html) +* [Эмуляция палитровой графики в WebGL](webgl-qna-emulating-palette-based-graphics-in-webgl.html) + +--- + +* [Простой способ показать нагрузку на вершинную и фрагментную обработку GPU?](webgl-qna-a-simple-way-to-show-the-load-on-the-gpu-s-vertex-and-fragment-processing-.html) +* [Как рисовать толстые линии в WebGL](https://mattdesl.svbtle.com/drawing-lines-is-hard) +* [FPS-подобное движение камеры с базовыми матричными трансформациями](webgl-qna-fps-like-camera-movement-with-basic-matrix-transformations.html) +* [Как использовать текстуру и цвет также в WebGL?](webgl-qna-how-to-use-texture--and-color-also-in-webgl-.html) +* [GLSL шейдер для поддержки раскрашивания и текстурирования](webgl-qna-glsl-shader-to-support-coloring-and-texturing.html) + +--- + +* [Как привязать массив текстур к uniform WebGL шейдера?](webgl-qna-how-to-bind-an-array-of-textures-to-a-webgl-shader-uniform-.html) +* [Передача значений альфа для каждого спрайта при батчинге](webgl-qna-passing-in-per-sprite-alpha-values-when-batching.html) +* [Может ли кто-нибудь объяснить, что делает этот GLSL фрагментный шейдер?](webgl-qna-can-anyone-explain-what-this-glsl-fragment-shader-is-doing-.html) +* [WebGL 2D тайлмапы](webgl-qna-webgl-2d-tilemaps.html) +* [Определение минимальных/максимальных значений для всего изображения](webgl-qna-determine-min-max-values-for-the-entire-image.html) + +--- + +* [Рисование множества различных моделей в одном вызове draw](webgl-qna-drawing-many-different-models-in-a-single-draw-call.html) +* [Запись FPS в WebGL](webgl-qna-recording-fps-in-webgl.html) +* [Как написать веб-визуализатор музыки](webgl-qna-how-to-write-a-web-based-music-visualizer.html) +* [Как получить аудио данные в шейдер](webgl-qna-how-to-get-audio-data-into-a-shader.html) +* [Чистая пунктирная линия WebGL](webgl-qna-pure-webgl-dashed-line.html) +* [Масштабирование и остановка на объекте в сцене в WebGL](webgl-qna-zooming-to-and-stopping-at-object-in-a-scene-in-webgl.html) + +--- + +* [Создание эффекта искажения изображения в WebGL](webgl-qna-create-image-warping-effect-in-webgl.html) +* [Создание эффекта размазывания/разжижения](webgl-qna-creating-a-smudge-liquify-effect.html) +* [Как получить эффект пикселизации в WebGL?](webgl-qna-how-to-get-pixelize-effect-in-webgl-.html) +* [Как сделать WebGL canvas прозрачным](webgl-qna-how-to-make-webgl-canvas-transparent.html) +* [Показать ночной вид против дневного вида на 3D сфере Земли](webgl-qna-show-a-night-view-vs-a-day-view-on-a-3d-earth-sphere.html) + +--- + +* [Возможно ли измерить время рендеринга в WebGL используя gl.finish()?](webgl-qna-is-it-possible-to-measure-rendering-time-in-webgl-using-gl-finish---.html) +* [Эффективная система частиц в JavaScript? (WebGL)](webgl-qna-efficient-particle-system-in-javascript---webgl-.html) +* [Установка значений массива структур из JS в GLSL](webgl-qna-setting-the-values-of-a-struct-array-from-js-to-glsl.html) +* [Как затухать буфер рисования](webgl-qna-how-to-fade-the-drawing-buffer.html) +* [Рисование 2D изображения с картой глубины для достижения псевдо-3D эффекта](webgl-qna-drawing-2d-image-with-depth-map-to-achieve-pseudo-3d-effect.html) + +--- + +* [Как достичь движущейся линии с эффектами следа](webgl-qna-how-to-achieve-moving-line-with-trail-effects.html) +* [Как правильно рисовать текстурированные трапециевидные полигоны](webgl-qna-how-to-draw-correctly-textured-trapezoid-polygons.html) +* [Tex image TEXTURE_2D level 0 вызывает ленивую инициализацию](webgl-qna-tex-image-texture_2d-level-0-is-incurring-lazy-initialization.html) +* [Как реализовать масштабирование от мыши в 2D WebGL](webgl-qna-how-to-implement-zoom-from-mouse-in-2d-webgl.html) +* [Как создать тор](webgl-qna-how-to-create-a-torus.html) + +--- + +* [Не смешивать полигон, который пересекает сам себя](webgl-qna-don-t-blend-a-polygon-that-crosses-itself.html) +* [Как обнаружить обрезанные треугольники во фрагментном шейдере](webgl-qna-how-to-detect-clipped-triangles-in-the-framgment-shader.html) +* [Самый быстрый способ нарисовать много кругов](webgl-qna-the-fastest-way-to-draw-many-circles.html) +* [Сортировка и оптимизация инстансированного рендеринга](webgl-qna-sorting-and-optimizing-instanced-rendering.html) +* [Как определить, есть ли у изображения альфа-канал](webgl-qna-how-to-tell-if-an-image-has-an-alpha-channel.html) + +--- + +* [Как загружать изображения в фоне без рывков](webgl-qna-how-to-load-images-in-the-background-with-no-jank.html) +* [Когда выбирать highp, mediump, lowp в шейдерах](webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.html) +* [Как импортировать карту высот в WebGL](webgl-qna-how-to-import-a-heightmap-in-webgl.html) +* [Применение карты смещения и карты бликов](webgl-qna-apply-a-displacement-map-and-specular-map.html) +* [Как поддерживать и WebGL, и WebGL2](webgl-qna-how-to-support-both-webgl-and-webgl2.html) + +--- + +* [Как использовать буфер трафарета](webgl-qna-how-to-use-the-stencil-buffer.html) +* [Рисование карты высот](webgl-qna-drawing-a-heightmap.html) +* [Рисование текстурированных спрайтов с инстансированным рисованием](webgl-qna-drawing-textured-sprites-with-instanced-drawing.html) +* [Оптимизация рисования множества больших изображений](webgl-qna-optimize-drawing-lots-of-large-images.html) +* [Рендеринг медленно со временем](webgl-qna-rendering-slowly-over-time.html) + +--- + +* [Получить размер точки для проверки столкновений](webgl-qna-get-the-size-of-a-point-for-collision-checking.html) +* [Как получить 3D координаты клика мыши](webgl-qna-how-to-get-the-3d-coordinates-of-a-mouse-click.html) +* [Как смешивать цвета между 2 треугольниками](webgl-qna-how-to-blend-colors-across-2-triangles.html) +* [Как использовать текстуры как данные](webgl-qna-how-to-use-textures-as-data.html) +* [Как использовать прозрачность 2D спрайта как маску](webgl-qna-how-to-use-a-2d-sprite-s-transparency-as-a-mask.html) + +--- + +* [Как предотвратить просачивание текстур с атласом текстур](webgl-qna-how-to-prevent-texture-bleeding-with-a-texture-atlas.html) +* [Как контролировать цвет между вершинами](webgl-qna-how-to-control-the-color-between-vertices.html) +* [Как сделать шейдер рыбьего глаза для скайбокса](webgl-qna-how-to-make-fisheye-skybox-shader.html) +* [Как получить автодополнение кода для WebGL в Visual Studio Code](webgl-qna-how-to-get-code-completion-for-webgl-in-visual-studio-code.html) +* [Рисование слоев с разными точками](webgl-qna-drawing-layers-with-different-points.html) + +--- + +* [Могу ли я отключить предупреждение о том, что vertex attrib 0 отключен?](webgl-qna-can-i-mute-the-warning-about-vertex-attrib-0-being-disabled-.html) +* [Как сделать инструмент размазывающей кисти](webgl-qna-how-to-make-a-smudge-brush-tool.html) +* [Как читать один компонент с readPixels](webgl-qna-how-to-read-a-single-component-with-readpixels.html) +* [Я получаю ошибку недопустимого типа при вызове readPixels](webgl-qna-i-get-invalid-type-error-when-calling-readpixels.html) +* [Как оптимизировать рендеринг UI](webgl-qna-how-to-optimize-rendering-a-ui.html) + +--- + +* [Как я могу вычислить для 500 точек, какая из 1000 отрезков линий ближе всего к каждой точке?](webgl-qna-how-can-i-compute-for-500-points-which-of-1000-line-segments-is-nearest-to-each-point-.html) +* [Как я могу переместить точку схода перспективы из центра canvas?](webgl-qna-how-can-i-move-the-perspective-vanishing-point-from-the-center-of-the-canvas-.html) +* [Существует ли понятие обобщенного вершинного и фрагментного шейдера?](webgl-qna-is-there-the-notion-of-a-generalized-vertex-and-fragment-shader-.html) +* [Как определить среднюю яркость в сцене?](webgl-qna-how-to-determine-the-average-brightness-in-a-scene-.html) +* [Что такое локальное начало координат 3D модели?](webgl-qna-what-is-the-local-origin-of-a-3d-model-.html) + +## WebGL2 + +* [Доступ к текстурам по координатам пикселей в WebGL2](webgl-qna-accessing-textures-by-pixel-coordinate-in-webgl2.html) +* [Как я могу получить все uniforms и uniformBlocks](webgl-qna-how-can-i-get-all-the-uniforms-and-uniformblocks.html) +* [Как я могу создать 16-битную гистограмму 16-битных данных](webgl-qna-how-can-i-create-a-16bit-historgram-of-16bit-data.html) +* [Как обрабатывать позиции частиц](webgl-qna-how-to-process-particle-positions.html) \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-readpixels.md b/webgl/lessons/ru/webgl-readpixels.md new file mode 100644 index 000000000..63c6a0604 --- /dev/null +++ b/webgl/lessons/ru/webgl-readpixels.md @@ -0,0 +1,37 @@ +Title: WebGL2 readPixels +Description: Детали о readPixels +TOC: readPixels + +В WebGL вы передаете пару format/type в `readPixels`. Для данного +внутреннего формата текстуры (прикрепленной к framebuffer), только 2 комбинации +format/type являются действительными. + +Из спецификации: + +> Для нормализованных поверхностей рендеринга с фиксированной точкой принимается комбинация format `RGBA` и type +`UNSIGNED_BYTE`. Для поверхностей рендеринга со знаковыми целыми числами принимается комбинация +format `RGBA_INTEGER` и type `INT`. Для поверхностей рендеринга с беззнаковыми целыми числами +принимается комбинация format `RGBA_INTEGER` и type `UNSIGNED_INT`. + +Вторая комбинация определяется реализацией +что, вероятно, означает, что вы не должны использовать ее в WebGL, если хотите, чтобы ваш код был переносимым. +Вы можете спросить, какая комбинация format/type, запросив + +```js +// предполагая, что framebuffer привязан с текстурой для чтения +const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); +const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); +``` + +Также обратите внимание, что форматы текстур, которые являются рендерируемыми, что означает, что вы можете прикрепить их к framebuffer и рендерить в них, +также в некоторой степени определяются реализацией. +WebGL2 перечисляет [много форматов](webgl-data-textures.html), но некоторые являются опциональными (`LUMINANCE`, например) и некоторые +не являются рендерируемыми по умолчанию, но могут быть сделаны рендерируемыми через расширение. (`RGBA32F`, например). + +**Таблица ниже живая**. Вы можете заметить, что она дает разные результаты в зависимости от машины, ОС, GPU или даже +браузера. Я знаю, что на моей машине Chrome и Firefox дают разные результаты для некоторых значений, определяемых реализацией. + +
+ + + \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-references.md b/webgl/lessons/ru/webgl-references.md new file mode 100644 index 000000000..2ad2d0527 --- /dev/null +++ b/webgl/lessons/ru/webgl-references.md @@ -0,0 +1,63 @@ +Title: Ссылки +Description: Другие ссылки +TOC: Ссылки + +Некоторые другие ссылки, которые могут оказаться полезными + +## Уроки и туториалы + +* [3d game shaders for beginners](https://lettier.github.io/3d-game-shaders-for-beginners/) + содержит много отличных объяснений многих графических техник. Основан на OpenGL, но + объяснения хорошо иллюстрированы, поэтому должно быть возможно адаптировать их + к WebGL. + +* [Learn OpenGL](https://learnopengl.com/): Современные уроки OpenGL + + Они могут быть или не быть полезными. Хотя API похожи, OpenGL - это не WebGL. Во-первых, OpenGL + - это библиотека на основе C. Другая проблема в том, что OpenGL имеет гораздо больше функций, чем WebGL, и + языки шейдеров имеют много различий. Тем не менее, многие идеи и техники, показанные здесь, + так же полезны в WebGL, как и в OpenGL. + +## Помощники / Расширения + +* [Spector](https://chrome.google.com/webstore/detail/spectorjs/denbgaamihkadbghdceggmchnflmhpmk?hl=en): Расширение для показа всех ваших WebGL вызовов + +* [Shader Editor](https://chrome.google.com/webstore/detail/shader-editor/ggeaidddejpbakgafapihjbgdlbbbpob?hl=en): Расширение, которое позволяет просматривать и редактировать шейдеры на живых веб-страницах. + +* [WebGL Insight](https://chrome.google.com/webstore/detail/webgl-insight/djdcbmfacaaocoomokenoalbomllhnko?hl=en): Расширение для просмотра использования WebGL + +* [webgl-helpers](https://greggman.github.io/webgl-helpers/): Скрипты для помощи с WebGL + +## Библиотеки + +* [twgl](https://twgljs.org): Библиотека для помощи в уменьшении многословности WebGL. + +* [three.js](https://threejs.org): самая популярная JavaScript 3D библиотека. + +* [PlayCanvas](https://playcanvas.com/) WebGL игровой движок с игровым редактором + +* [regl](https://regl.party/): Функциональная WebGL библиотека без состояния. + +## Спецификации + +* [WebGL2](https://www.khronos.org/registry/webgl/specs/latest/2.0/): Спецификация WebGL2 + +* [OpenGL ES 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/es_spec_3.0.pdf): Спецификация, на которой основан WebGL2. + +* [GLSL ES 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf): Спецификация языка шейдеров для WebGL2 + +## Развлечения + +* [Shadertoy.com](https://shadertoy.com): Удивительные фрагментные шейдеры, созданные в экстремальных ограничениях + + Предупреждение: Шейдеры на shadertoy.com обычно не являются тем типом шейдеров, которые используются в продакшн + WebGL приложениях. Тем не менее, есть много техник, которые можно изучить из их примеров. + +* [glslsandbox.com](https://glslsandbox.com): Оригинальная песочница фрагментных шейдеров. + +* [vertexshaerart.com](https://vertexshaderart.com): Версия glslsandbox для вершинных шейдеров. + +--- + +Если вы знаете другие хорошие ссылки для добавления, не стесняйтесь +[открыть issue](https://github.com/gfxfundamentals/webgl-fundamentals/issues). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-render-to-texture.md b/webgl/lessons/ru/webgl-render-to-texture.md new file mode 100644 index 000000000..8bb564fcc --- /dev/null +++ b/webgl/lessons/ru/webgl-render-to-texture.md @@ -0,0 +1,331 @@ +Title: WebGL2 Рендеринг в текстуру +Description: Как рендерить в текстуру. +TOC: Рендеринг в текстуру + +Этот пост является продолжением серии постов о WebGL2. +Первый [начинался с основ](webgl-fundamentals.html) и +предыдущий был о [предоставлении данных текстурам](webgl-data-textures.html). +Если вы не читали их, пожалуйста, просмотрите их сначала. + +В последнем посте мы рассмотрели, как предоставлять данные из JavaScript в текстуры. +В этой статье мы будем рендерить в текстуры, используя WebGL2. Обратите внимание, что эта тема +была кратко рассмотрена в [обработке изображений](webgl-image-processing-continued.html), но +давайте рассмотрим ее более подробно. + +Рендеринг в текстуру довольно прост. Мы создаем текстуру определенного размера + + // создаем для рендеринга + const targetTextureWidth = 256; + const targetTextureHeight = 256; + const targetTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, targetTexture); + + { + // определяем размер и формат уровня 0 + const level = 0; + const internalFormat = gl.RGBA; + const border = 0; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; + const data = null; + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, + targetTextureWidth, targetTextureHeight, border, + format, type, data); + + // устанавливаем фильтрацию, чтобы нам не нужны были мипмапы + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } + +Обратите внимание, как `data` равен `null`. Нам не нужно предоставлять никаких данных. Нам просто нужно, чтобы WebGL +выделил текстуру. + +Далее мы создаем framebuffer. [Framebuffer - это просто коллекция вложений](webgl-framebuffers.html). Вложения +- это либо текстуры, либо renderbuffer. Мы уже рассматривали текстуры. Renderbuffer очень похож +на текстуры, но они поддерживают форматы и опции, которые текстуры не поддерживают. Также, в отличие от текстуры, +вы не можете напрямую использовать renderbuffer как вход для шейдера. + +Давайте создадим framebuffer и прикрепим нашу текстуру + + // Создаем и привязываем framebuffer + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + + // прикрепляем текстуру как первое цветовое вложение + const attachmentPoint = gl.COLOR_ATTACHMENT0; + gl.framebufferTexture2D( + gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level); + +Так же, как текстуры и буферы, после создания framebuffer нам нужно +привязать его к точке привязки `FRAMEBUFFER`. После этого все функции, связанные с +framebuffer, ссылаются на любой framebuffer, который привязан там. + +С нашим привязанным framebuffer, каждый раз, когда мы вызываем `gl.clear`, `gl.drawArrays` или `gl.drawElements`, WebGL +будет рендерить в нашу текстуру вместо холста. + +Давайте возьмем наш предыдущий код рендеринга и сделаем его функцией, чтобы мы могли вызывать его дважды. +Один раз для рендеринга в текстуру и снова для рендеринга в холст. + +``` +function drawCube(aspect) { + // Говорим использовать нашу программу (пару шейдеров) + gl.useProgram(program); + + // Привязываем набор атрибутов/буферов, который мы хотим. + gl.bindVertexArray(vao); + + // Вычисляем матрицу проекции + - var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + var projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, 1, 2000); + + var cameraPosition = [0, 0, 2]; + var up = [0, 1, 0]; + var target = [0, 0, 0]; + + // Вычисляем матрицу камеры, используя look at. + var cameraMatrix = m4.lookAt(cameraPosition, target, up); + + // Создаем матрицу вида из матрицы камеры. + var viewMatrix = m4.inverse(cameraMatrix); + + var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix); + + var matrix = m4.xRotate(viewProjectionMatrix, modelXRotationRadians); + matrix = m4.yRotate(matrix, modelYRotationRadians); + + // Устанавливаем матрицу. + gl.uniformMatrix4fv(matrixLocation, false, matrix); + + // Говорим шейдеру использовать текстуру unit 0 для u_texture + gl.uniform1i(textureLocation, 0); + + // Рисуем геометрию. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 6 * 6; + gl.drawArrays(primitiveType, offset, count); +} +``` + +Обратите внимание, что нам нужно передать `aspect` для вычисления нашей матрицы проекции, +потому что наша целевая текстура имеет другой аспект, чем камера. + +Вот как мы вызываем это + +``` +// Рисуем сцену. +function drawScene(time) { + + ... + + { + // рендерим в наш targetTexture, привязывая framebuffer + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + + // рендерим куб с нашей текстурой 3x2 + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Говорим WebGL, как конвертировать из пространства отсечения в пиксели + gl.viewport(0, 0, targetTextureWidth, targetTextureHeight); + + // Очищаем холст И буфер глубины. + gl.clearColor(0, 0, 1, 1); // очищаем до синего + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const aspect = targetTextureWidth / targetTextureHeight; + drawCube(aspect) + } + + { + // рендерим в холст + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + // рендерим куб с текстурой, в которую мы только что рендерили + gl.bindTexture(gl.TEXTURE_2D, targetTexture); + + // Говорим WebGL, как конвертировать из пространства отсечения в пиксели + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Очищаем холст И буфер глубины. + gl.clearColor(1, 1, 1, 1); // очищаем до белого + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + drawCube(aspect) + } + + requestAnimationFrame(drawScene); +} +``` + +И вот результат + +{{{example url="../webgl-render-to-texture.html" }}} + +**КРАЙНЕ ВАЖНО** помнить о вызове `gl.viewport` и установке его в +размер того, во что вы рендерите. В этом случае первый раз мы +рендерим в текстуру, поэтому мы устанавливаем viewport, чтобы покрыть текстуру. Второй +раз мы рендерим в холст, поэтому мы устанавливаем viewport, чтобы покрыть холст. + +Аналогично, когда мы вычисляем матрицу проекции, +нам нужно использовать правильный аспект для того, во что мы рендерим. Я потерял бесчисленные +часы отладки, задаваясь вопросом, почему что-то рендерится забавно или не рендерится +совсем, только чтобы в конечном итоге обнаружить, что я забыл один или оба вызова `gl.viewport` +и вычисление правильного аспекта. Это так легко забыть, что теперь я стараюсь никогда не вызывать +`gl.bindFramebuffer` в своем коде напрямую. Вместо этого я делаю функцию, которая делает и то, и другое, +что-то вроде + + function bindFramebufferAndSetViewport(fb, width, height) { + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.viewport(0, 0, width, height); + } + +И тогда я использую только эту функцию для изменения того, во что я рендерю. Таким образом я не забуду. + +Одна вещь, которую нужно заметить, это то, что у нас нет буфера глубины на нашем framebuffer. У нас есть только текстура. +Это означает, что нет тестирования глубины и 3D не будет работать. Если мы нарисуем 3 куба, мы можем увидеть это. + +{{{example url="../webgl-render-to-texture-3-cubes-no-depth-buffer.html" }}} + +Если вы посмотрите на центральный куб, вы увидите, что 3 вертикальных куба рисуются на нем, один сзади, один в середине +и еще один спереди, но мы рисуем все 3 на одной глубине. Глядя на 3 горизонтальных куба, +нарисованных на холсте, вы заметите, что они правильно пересекают друг друга. Это потому, что наш framebuffer +не имеет буфера глубины, но наш холст имеет. + + + +Чтобы добавить буфер глубины, мы создаем текстуру глубины и прикрепляем ее к нашему framebuffer. + +``` +// создаем текстуру глубины +const depthTexture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, depthTexture); + +// делаем буфер глубины того же размера, что и targetTexture +{ + // определяем размер и формат уровня 0 + const level = 0; + const internalFormat = gl.DEPTH_COMPONENT24; + const border = 0; + const format = gl.DEPTH_COMPONENT; + const type = gl.UNSIGNED_INT; + const data = null; + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, + targetTextureWidth, targetTextureHeight, border, + format, type, data); + + // устанавливаем фильтрацию, чтобы нам не нужны были мипмапы + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + // прикрепляем текстуру глубины к framebuffer + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, level); +} +``` + +И с этим вот результат. + +{{{example url="../webgl-render-to-texture-3-cubes-with-depth-buffer.html" }}} + +Теперь, когда у нас есть буфер глубины, прикрепленный к нашему framebuffer, внутренние кубы правильно пересекаются. + + + +Важно отметить, что WebGL гарантирует работу только определенных комбинаций вложений. +[Согласно спецификации](https://www.khronos.org/registry/webgl/specs/latest/1.0/#FBO_ATTACHMENTS) +единственные гарантированные комбинации вложений: + +* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура +* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура + `DEPTH_ATTACHMENT` = `DEPTH_COMPONENT16` renderbuffer +* `COLOR_ATTACHMENT0` = `RGBA/UNSIGNED_BYTE` текстура + `DEPTH_STENCIL_ATTACHMENT` = `DEPTH_STENCIL` renderbuffer + +Для любых других комбинаций вы должны проверить, поддерживает ли система/GPU/драйвер/браузер пользователя эту комбинацию. +Чтобы проверить, вы создаете свой framebuffer, создаете и прикрепляете вложения, затем вызываете + + var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + +Если статус `FRAMEBUFFER_COMPLETE`, то эта комбинация вложений работает для этого пользователя. +В противном случае она не работает, и вам придется сделать что-то еще, например, сказать пользователю, что ему не повезло, +или переключиться на какой-то другой метод. + +Если вы еще не проверили [упрощение WebGL с меньшим количеством кода больше веселья](webgl-less-code-more-fun.html). + +
+

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

+

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

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

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

+

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

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-resizing-the-canvas.md b/webgl/lessons/ru/webgl-resizing-the-canvas.md new file mode 100644 index 000000000..f5828a93f --- /dev/null +++ b/webgl/lessons/ru/webgl-resizing-the-canvas.md @@ -0,0 +1,204 @@ +Title: WebGL2 Изменение размера Canvas. +Description: Как изменить размер WebGL canvas и связанные с этим проблемы +TOC: Изменение размера Canvas + +Вот что вам нужно знать, чтобы изменить размер canvas. + +У каждого canvas есть 2 размера. Размер его drawingbuffer. Это сколько пикселей в canvas. +Второй размер - это размер, в котором отображается canvas. CSS определяет размер, в котором canvas +отображается. + +Вы можете установить размер drawingbuffer canvas двумя способами. Один используя HTML + +```html + +``` + +Другой используя JavaScript + +```html + +``` + +JavaScript + +```js +const canvas = document.querySelector("#c"); +canvas.width = 400; +canvas.height = 300; +``` + +Что касается установки размера отображения canvas, если у вас нет CSS, который влияет на размер отображения canvas, +размер отображения будет таким же, как размер его drawingbuffer. Поэтому в 2 примерах выше drawingbuffer canvas составляет 400x300, +и его размер отображения также 400x300. + +Вот пример canvas, чей drawingbuffer составляет 10x15 пикселей, который отображается 400x300 пикселей на странице + +```html + +``` + +или, например, так + +```html + + +``` + +Если мы нарисуем одну пиксельную вращающуюся линию в этот canvas, мы увидим что-то вроде этого + +{{{example url="../webgl-10x15-canvas-400x300-css.html" }}} + +Почему это так размыто? Потому что браузер берет наш 10x15 пиксельный canvas и растягивает его до 400x300 пикселей, и +обычно фильтрует его при растягивании. + +Итак, что мы делаем, если, например, хотим, чтобы canvas заполнил окно? Ну, сначала мы можем заставить +браузер растянуть canvas, чтобы заполнить окно с помощью CSS. Пример + + + + + + + + + + +Теперь нам просто нужно сделать drawingbuffer соответствующим любому размеру, до которого браузер растянул canvas. +Это, к сожалению, сложная тема. Давайте рассмотрим некоторые различные методы + +## Использование `clientWidth` и `clientHeight` + +Это самый простой способ. +`clientWidth` и `clientHeight` - это свойства, которые есть у каждого элемента в HTML, которые говорят нам +размер элемента в CSS пикселях. + +> Примечание: Client rect включает любой CSS padding, поэтому если вы используете `clientWidth` +и/или `clientHeight`, лучше не ставить никакой padding на ваш canvas элемент. + +Используя JavaScript, мы можем проверить, какого размера этот элемент отображается, а затем настроить +размер его drawingbuffer, чтобы соответствовать. + +```js +function resizeCanvasToDisplaySize(canvas) { + // Ищем размер, в котором браузер отображает canvas в CSS пикселях. + const displayWidth = canvas.clientWidth; + const displayHeight = canvas.clientHeight; + + // Проверяем, не является ли canvas того же размера. + const needResize = canvas.width !== displayWidth || + canvas.height !== displayHeight; + + if (needResize) { + // Делаем canvas того же размера + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + return needResize; +} +``` + +Давайте вызовем эту функцию прямо перед рендерингом, +чтобы она всегда настраивала canvas до нашего желаемого размера прямо перед рисованием. + +```js +function drawScene() { + resizeCanvasToDisplaySize(gl.canvas); + + ... +``` + +И вот это + +{{{example url="../webgl-resize-canvas.html" }}} + +Эй, что-то не так? Почему линия не покрывает всю область? + +Причина в том, что когда мы изменяем размер canvas, нам также нужно вызвать `gl.viewport`, чтобы установить viewport. +`gl.viewport` говорит WebGL, как конвертировать из пространства отсечения (-1 до +1) обратно в пиксели и где это делать +внутри canvas. Когда вы впервые создаете WebGL контекст, WebGL установит viewport, чтобы соответствовать размеру +canvas, но после этого вам нужно установить его. Если вы изменяете размер canvas, +вам нужно сказать WebGL новую настройку viewport. + +Давайте изменим код, чтобы обработать это. Помимо этого, поскольку WebGL контекст имеет +ссылку на canvas, давайте передадим это в resize. + + function drawScene() { + resizeCanvasToDisplaySize(gl.canvas); + + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + ... + +Теперь это работает. + +{{{example url="../webgl-resize-canvas-viewport.html" }}} + +Откройте это в отдельном окне, измените размер окна, обратите внимание, что оно всегда заполняет окно. + +Я слышу, как вы спрашиваете, *почему WebGL не устанавливает viewport автоматически для нас, +когда мы изменяем размер canvas?* Причина в том, что он не знает, как или почему +вы используете viewport. Вы могли бы [рендерить в framebuffer](webgl-render-to-texture.html) +или делать что-то еще, что требует другого размера viewport. WebGL не имеет +способа узнать ваше намерение, поэтому он не может автоматически установить viewport для вас. + +--- + +## Обработка `devicePixelRatio` и масштабирования + +Почему это не конец? Ну, здесь становится сложно. + +Первое, что нужно понять, это то, что большинство размеров в браузере в CSS пиксельных +единицах. Это попытка сделать размеры независимыми от устройства. Так, например, +в начале этой статьи мы установили размер отображения canvas в 400x300 CSS +пикселей. В зависимости от того, есть ли у пользователя HD-DPI дисплей, или он увеличен или +уменьшен, или имеет установленный уровень масштабирования ОС, сколько фактических пикселей это станет на +мониторе, будет разным. + +`window.devicePixelRatio` скажет нам в общем соотношение фактических пикселей +к CSS пикселям на вашем мониторе. Например, вот текущая настройка вашего браузера + +>
devicePixelRatio =
+ +Если вы на настольном компьютере или ноутбуке, попробуйте нажать ctrl++ и ctrl+-, чтобы увеличить и уменьшить (++ и +- на Mac). Вы должны увидеть, как число изменяется. + +Итак, если мы хотим, чтобы количество пикселей в canvas соответствовало количеству пикселей, фактически используемых для его отображения, +казалось бы очевидным решением было бы умножить `clientWidth` и `clientHeight` на `devicePixelRatio`, как это: + +```js +function resizeCanvasToDisplaySize(canvas) { + // Ищем размер, в котором браузер отображает canvas в CSS пикселях. + const dpr = window.devicePixelRatio; + const displayWidth = Math.round(canvas.clientWidth * dpr); + const displayHeight = Math.round(canvas.clientHeight * dpr); + + // Проверяем, не является ли canvas того же размера. + const needResize = canvas.width != displayWidth || + canvas.height != displayHeight; + + if (needResize) { + // Делаем canvas того же размера + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + return needResize; +} +``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-scene-graph.md b/webgl/lessons/ru/webgl-scene-graph.md new file mode 100644 index 000000000..87a68a86e --- /dev/null +++ b/webgl/lessons/ru/webgl-scene-graph.md @@ -0,0 +1,460 @@ +Title: WebGL2 - Граф сцены +Description: Что такое граф сцены и для чего он используется +TOC: Графы сцен + + +Эта статья является продолжением [предыдущих статей WebGL](webgl-fundamentals.html). +Предыдущая статья была о [рисовании множественных объектов](webgl-drawing-multiple-things.html). +Если вы их не читали, я предлагаю начать с них. + +Я уверен, что какой-нибудь гуру CS или графики даст мне нагоняй, но... +Граф сцены обычно представляет собой древовидную структуру, где каждый узел в дереве генерирует +матрицу... хм, это не очень полезное определение. Может быть, несколько примеров будут +полезны. + +Большинство 3D движков используют граф сцены. Вы помещаете объекты, которые хотите видеть в сцене, +в граф сцены. Движок затем обходит граф сцены и составляет список объектов для рисования. +Графы сцен иерархичны, поэтому, например, если вы хотели бы создать симуляцию вселенной, +вы могли бы иметь граф, который выглядит так + +{{{diagram url="resources/planet-diagram.html" height="500" }}} + +В чем смысл графа сцены? Главная особенность графа сцены заключается в том, что он обеспечивает +родительско-дочерние отношения для матриц, как [мы обсуждали в 2D матричной математике](webgl-2d-matrices.html). +Так, например, в простой (но нереалистичной) симуляции вселенной звезды (дети) движутся вместе со своей +галактикой (родитель). Аналогично луна (ребенок) движется вместе со своей планетой (родитель). +Если вы переместите Землю, луна будет двигаться с ней. Если вы переместите галактику, +все звезды внутри будут двигаться с ней. Перетащите имена в диаграмме выше +и, надеюсь, вы сможете увидеть их отношения. + +Если вы вернетесь к [2D матричной математике](webgl-2d-matrices.html), вы можете вспомнить, что мы +умножаем много матриц для перемещения, поворота и масштабирования объектов. Граф +сцены предоставляет структуру для помощи в решении, какую матричную математику применять к объекту. + +Обычно каждый `Node` в графе сцены представляет *локальное пространство*. При правильной +матричной математике все в этом *локальном пространстве* может игнорировать все выше него. Другой +способ выразить то же самое - луна должна заботиться только об орбите вокруг Земли. +Ей не нужно заботиться об орбите вокруг Солнца. Без этой структуры графа сцены +вам пришлось бы делать гораздо более сложную математику для вычисления, как заставить луну +орбитировать вокруг Солнца, потому что ее орбита вокруг Солнца выглядит примерно так + +{{{diagram url="resources/moon-orbit.html" }}} + +С графом сцены вы просто делаете луну дочерним элементом Земли, а затем орбитируете +вокруг Земли, что просто. Граф сцены заботится о том факте, что Земля +орбитирует вокруг Солнца. Он делает это, обходя узлы и умножая +матрицы по мере обхода + + worldMatrix = greatGrandParent * grandParent * parent * self(localMatrix) + +В конкретных терминах нашей симуляции вселенной это было бы + + worldMatrixForMoon = galaxyMatrix * starMatrix * planetMatrix * moonMatrix; + +Мы можем сделать это очень просто с рекурсивной функцией, которая эффективно + + function computeWorldMatrix(currentNode, parentWorldMatrix) { + // вычисляем нашу мировую матрицу, умножая нашу локальную матрицу на + // мировую матрицу нашего родителя. + var worldMatrix = m4.multiply(parentWorldMatrix, currentNode.localMatrix); + + // теперь делаем то же самое для всех наших детей + currentNode.children.forEach(function(child) { + computeWorldMatrix(child, worldMatrix); + }); + } + +Это поднимает некоторую терминологию, которая довольно распространена для 3D графов сцен. + +* `localMatrix`: Локальная матрица для текущего узла. Она трансформирует его и его детей в локальном пространстве с + самим собой в качестве начала координат. + +* `worldMatrix`: Для данного узла она берет объекты в локальном пространстве этого узла + и трансформирует их в пространство корневого узла графа сцены. Или другими словами, размещает их + в мире. Если мы вычислим worldMatrix для луны, мы получим ту забавную орбиту, которую вы видите выше. + +Граф сцены довольно легко создать. Давайте определим простой объект `Node`. +Есть миллион способов организовать граф сцены, и я не уверен, какой +способ лучше. Самый распространенный - иметь опциональное поле объекта для рисования + + var node = { + localMatrix: ..., // "локальная" матрица для этого узла + worldMatrix: ..., // "мировая" матрица для этого узла + children: [], // массив детей + thingToDraw: ??, // объект для рисования в этом узле + }; + +Давайте создадим граф сцены солнечной системы. Я не буду использовать причудливые текстуры или +что-то подобное, так как это загромоздит пример. Сначала давайте создадим несколько функций +для помощи в управлении узлами. Сначала мы создадим класс узла + + var Node = function() { + this.children = []; + this.localMatrix = m4.identity(); + this.worldMatrix = m4.identity(); + }; + +Давайте дадим ему способ установить родителя узла. + + Node.prototype.setParent = function(parent) { + // удаляем нас из нашего родителя + if (this.parent) { + var ndx = this.parent.children.indexOf(this); + if (ndx >= 0) { + this.parent.children.splice(ndx, 1); + } + } + + // Добавляем нас к нашему новому родителю + if (parent) { + parent.children.push(this); + } + this.parent = parent; + }; + +И вот код для вычисления мировых матриц из локальных матриц на основе их родительско-дочерних +отношений. Если мы начнем с родителя и рекурсивно посетим детей, мы сможем вычислить +их мировые матрицы. Если вы не понимаете матричную математику, +[проверьте эту статью о них](webgl-2d-matrices.html). + + Node.prototype.updateWorldMatrix = function(parentWorldMatrix) { + if (parentWorldMatrix) { + // была передана матрица, поэтому делаем математику и + // сохраняем результат в `this.worldMatrix`. + m4.multiply(parentWorldMatrix, this.localMatrix, this.worldMatrix); + } else { + // матрица не была передана, поэтому просто копируем. + m4.copy(this.localMatrix, this.worldMatrix); + } + + // теперь обрабатываем всех детей + var worldMatrix = this.worldMatrix; + this.children.forEach(function(child) { + child.updateWorldMatrix(worldMatrix); + }); + }; + +Давайте просто сделаем Солнце, Землю и Луну, чтобы держать это простым. Мы, конечно, будем использовать +фальшивые расстояния, чтобы вещи помещались на экране. Мы просто будем использовать одну модель сферы +и окрасим ее желтоватой для Солнца, сине-зеленоватой для Земли и сероватой для Луны. +Если `drawInfo`, `bufferInfo` и `programInfo` вам не знакомы, [см. предыдущую статью](webgl-drawing-multiple-things.html). + + // Давайте создадим все узлы + var sunNode = new Node(); + sunNode.localMatrix = m4.translation(0, 0, 0); // солнце в центре + sunNode.drawInfo = { + uniforms: { + u_colorOffset: [0.6, 0.6, 0, 1], // желтый + u_colorMult: [0.4, 0.4, 0, 1], + }, + programInfo: programInfo, + bufferInfo: sphereBufferInfo, + vertexArray: sphereVAO, + }; + + var earthNode = new Node(); + earthNode.localMatrix = m4.translation(100, 0, 0); // земля в 100 единицах от солнца + earthNode.drawInfo = { + uniforms: { + u_colorOffset: [0.2, 0.5, 0.8, 1], // сине-зеленый + u_colorMult: [0.8, 0.5, 0.2, 1], + }, + programInfo: programInfo, + bufferInfo: sphereBufferInfo, + vertexArray: sphereVAO, + }; + + var moonNode = new Node(); + moonNode.localMatrix = m4.translation(20, 0, 0); // луна в 20 единицах от земли + moonNode.drawInfo = { + uniforms: { + u_colorOffset: [0.6, 0.6, 0.6, 1], // серый + u_colorMult: [0.1, 0.1, 0.1, 1], + }, + programInfo: programInfo, + bufferInfo: sphereBufferInfo, + vertexArray: sphereVAO, + }; + +Теперь, когда мы создали узлы, давайте соединим их. + + // соединяем небесные объекты + moonNode.setParent(earthNode); + earthNode.setParent(sunNode); + +Мы снова создадим список объектов и список объектов для рисования. + + var objects = [ + sunNode, + earthNode, + moonNode, + ]; + + var objectsToDraw = [ + sunNode.drawInfo, + earthNode.drawInfo, + moonNode.drawInfo, + ]; + +Во время рендеринга мы будем обновлять локальную матрицу каждого объекта, слегка поворачивая его. + + // обновляем локальные матрицы для каждого объекта. + m4.multiply(m4.yRotation(0.01), sunNode.localMatrix , sunNode.localMatrix); + m4.multiply(m4.yRotation(0.01), earthNode.localMatrix, earthNode.localMatrix); + m4.multiply(m4.yRotation(0.01), moonNode.localMatrix , moonNode.localMatrix); + +Теперь, когда локальные матрицы обновлены, мы обновим все мировые матрицы + + sunNode.updateWorldMatrix(); + +Наконец, теперь, когда у нас есть мировые матрицы, нам нужно умножить их, чтобы получить [матрицу worldViewProjection](webgl-3d-perspective.html) для каждого объекта. + + // Вычисляем все матрицы для рендеринга + objects.forEach(function(object) { + object.drawInfo.uniforms.u_matrix = m4.multiply(viewProjectionMatrix, object.worldMatrix); + }); + +Рендеринг - это [тот же цикл, который мы видели в нашей последней статье](webgl-drawing-multiple-things.html). + +{{{example url="../webgl-scene-graph-solar-system.html" }}} + +Вы заметите, что все планеты одинакового размера. Давайте попробуем сделать Землю больше + + // земля в 100 единицах от солнца + earthNode.localMatrix = m4.translation(100, 0, 0)); + + // делаем землю в два раза больше + earthNode.localMatrix = m4.scale(earthNode.localMatrix, 2, 2, 2); + +{{{example url="../webgl-scene-graph-solar-system-larger-earth.html" }}} + +Упс. Луна тоже стала больше. Чтобы исправить это, мы могли бы вручную уменьшить луну. Лучшее решение, однако, +заключается в добавлении большего количества узлов в наш граф сцены. Вместо просто + + солнце + | + земля + | + луна + +Мы изменим это на + + солнечнаяСистема + | | + | солнце + | + орбитаЗемли + | | + | земля + | + орбитаЛуны + | + луна + +Это позволит Земле вращаться вокруг солнечнойСистемы, но мы можем отдельно вращать и масштабировать Солнце, и это не будет +влиять на Землю. Аналогично Земля может вращаться отдельно от Луны. Давайте создадим больше узлов для +`solarSystem`, `earthOrbit` и `moonOrbit`. + + var solarSystemNode = new Node(); + var earthOrbitNode = new Node(); + + // орбита земли в 100 единицах от солнца + earthOrbitNode.localMatrix = m4.translation(100, 0, 0); + var moonOrbitNode = new Node(); + + // луна в 20 единицах от земли + moonOrbitNode.localMatrix = m4.translation(20, 0, 0); + +Те расстояния орбит были удалены из старых узлов + + var earthNode = new Node(); + -// земля в 100 единицах от солнца + -earthNode.localMatrix = m4.translation(100, 0, 0)); + + -// делаем землю в два раза больше + -earthNode.localMatrix = m4.scale(earthNode.localMatrix, 2, 2, 2); + +earthNode.localMatrix = m4.scaling(2, 2, 2); + + var moonNode = new Node(); + -moonNode.localMatrix = m4.translation(20, 0, 0); // луна в 20 единицах от земли + +Соединение их теперь выглядит так + + // соединяем небесные объекты + sunNode.setParent(solarSystemNode); + earthOrbitNode.setParent(solarSystemNode); + earthNode.setParent(earthOrbitNode); + moonOrbitNode.setParent(earthOrbitNode); + moonNode.setParent(moonOrbitNode); + +И нам нужно обновлять только орбиты + + // обновляем локальные матрицы для каждого объекта. + -m4.multiply(m4.yRotation(0.01), sunNode.localMatrix , sunNode.localMatrix); + -m4.multiply(m4.yRotation(0.01), earthNode.localMatrix, earthNode.localMatrix); + -m4.multiply(m4.yRotation(0.01), moonNode.localMatrix , moonNode.localMatrix); + +m4.multiply(m4.yRotation(0.01), earthOrbitNode.localMatrix, earthOrbitNode.localMatrix); + +m4.multiply(m4.yRotation(0.01), moonOrbitNode.localMatrix, moonOrbitNode.localMatrix); + + // Обновляем все мировые матрицы в графе сцены + -sunNode.updateWorldMatrix(); + +solarSystemNode.updateWorldMatrix(); + +И теперь вы можете видеть, что Земля в два раза больше, а Луна нет. + +{{{example url="../webgl-scene-graph-solar-system-larger-earth-fixed.html" }}} + +Вы также можете заметить, что Солнце и Земля больше не вращаются на месте. Теперь это независимо. + +Давайте настроим еще несколько вещей. + + -sunNode.localMatrix = m4.translation(0, 0, 0); // солнце в центре + +sunNode.localMatrix = m4.scaling(5, 5, 5); + + ... + + *moonOrbitNode.localMatrix = m4.translation(30, 0, 0); + + ... + + +moonNode.localMatrix = m4.scaling(0.4, 0.4, 0.4); + + ... + // обновляем локальные матрицы для каждого объекта. + m4.multiply(m4.yRotation(0.01), earthOrbitNode.localMatrix, earthOrbitNode.localMatrix); + m4.multiply(m4.yRotation(0.01), moonOrbitNode.localMatrix, moonOrbitNode.localMatrix); + +// вращаем солнце + +m4.multiply(m4.yRotation(0.005), sunNode.localMatrix, sunNode.localMatrix); + +// вращаем землю + +m4.multiply(m4.yRotation(0.05), earthNode.localMatrix, earthNode.localMatrix); + +// вращаем луну + +m4.multiply(m4.yRotation(-0.01), moonNode.localMatrix, moonNode.localMatrix); + +{{{example url="../webgl-scene-graph-solar-system-adjusted.html" }}} + +В настоящее время у нас есть `localMatrix`, и мы модифицируем его каждый кадр. Однако есть проблема +в том, что каждый кадр наша математика будет накапливать небольшую ошибку. Есть способ исправить математику, +который называется *ортогональной нормализацией матрицы*, но даже это не всегда будет работать. Например, давайте +представим, что мы масштабировали до нуля и обратно. Давайте просто сделаем это для одного значения `x` + + x = 246; // кадр #0, x = 246 + + scale = 1; + x = x * scale // кадр #1, x = 246 + + scale = 0.5; + x = x * scale // кадр #2, x = 123 + + scale = 0; + x = x * scale // кадр #3, x = 0 + + scale = 0.5; + x = x * scale // кадр #4, x = 0 УПС! + + scale = 1; + x = x * scale // кадр #5, x = 0 УПС! + +Мы потеряли наше значение. Мы можем исправить это, добавив какой-то другой класс, который обновляет матрицу из +других значений. Давайте изменим определение `Node`, чтобы иметь `source`. Если он существует, мы будем +просить `source` дать нам локальную матрицу. + + *var Node = function(source) { + this.children = []; + this.localMatrix = makeIdentity(); + this.worldMatrix = makeIdentity(); + + this.source = source; + }; + + Node.prototype.updateWorldMatrix = function(matrix) { + + + var source = this.source; + + if (source) { + + source.getMatrix(this.localMatrix); + + } + + ... + +Теперь мы можем создать источник. Общий источник - это тот, который предоставляет перемещение, поворот и масштаб +что-то вроде этого + + var TRS = function() { + this.translation = [0, 0, 0]; + this.rotation = [0, 0, 0]; + this.scale = [1, 1, 1]; + }; + + TRS.prototype.getMatrix = function(dst) { + dst = dst || new Float32Array(16); + var t = this.translation; + var r = this.rotation; + var s = this.scale; + + // вычисляем матрицу из перемещения, поворота и масштаба + m4.translation(t[0], t[1], t[2], dst); + m4.xRotate(dst, r[0], dst); + m4.yRotate(dst, r[1], dst); + m4.zRotate(dst, r[2], dst); + m4.scale(dst, s[0], s[1], s[2]), dst); + return dst; + }; + +И мы можем использовать это так + + // во время инициализации создаем узел с источником + var someTRS = new TRS(); + var someNode = new Node(someTRS); + + // во время рендеринга + someTRS.rotation[2] += elapsedTime; + +Теперь нет проблемы, потому что мы воссоздаем матрицу каждый раз. + +Вы можете думать, я не создаю солнечную систему, так в чем смысл? Ну, если вы хотели бы +анимировать человека, вы могли бы иметь граф сцены, который выглядит так + +{{{diagram url="resources/person-diagram.html" height="400" }}} + +Сколько суставов вы добавляете для пальцев и пальцев ног, зависит от вас. Чем больше суставов у вас есть, +тем больше мощности требуется для вычисления анимаций и тем больше данных анимации требуется +для предоставления информации для всех суставов. Старые игры, такие как Virtua Fighter, имели около 15 суставов. +Игры в начале-середине 2000-х имели от 30 до 70 суставов. Если бы вы сделали каждый сустав в ваших руках, +там по крайней мере 20 в каждой руке, так что только 2 руки - это 40 суставов. Многие игры, которые хотят +анимировать руки, анимируют большой палец как один и 4 пальца как один большой палец, чтобы сэкономить +время (как CPU/GPU, так и время художника) и память. + +В любом случае, вот блочный парень, которого я собрал. Он использует источник `TRS` для каждого +узла, упомянутого выше. Программистское искусство и программистская анимация FTW! 😂 + +{{{example url="../webgl-scene-graph-block-guy.html" }}} + +Если вы посмотрите практически на любую 3D библиотеку, вы найдете граф сцены, подобный этому. +Что касается построения иерархий, обычно они создаются в каком-то пакете моделирования +или пакете компоновки уровней. + +
+

SetParent vs AddChild / RemoveChild

+

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

+
{{#escapehtml}}
+    someParent.addChild(someNode);
+    ...
+    someOtherParent.addChild(someNode);
+{{/escapehtml}}
+

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

+

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

+
{{#escapehtml}}
+    someNode.setParent(someParent);
+    ...
+    someNode.setParent(someOtherParent);
+{{/escapehtml}}
+

+В этом случае на 100% очевидно, что происходит. Нулевая неоднозначность. +

+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-setup-and-installation.md b/webgl/lessons/ru/webgl-setup-and-installation.md new file mode 100644 index 000000000..2143b5323 --- /dev/null +++ b/webgl/lessons/ru/webgl-setup-and-installation.md @@ -0,0 +1,172 @@ +Title: WebGL2 Настройка и установка +Description: Как заниматься разработкой WebGL +TOC: Настройка и установка + + +Технически вам не нужно ничего, кроме веб-браузера, чтобы заниматься WebGL +разработкой. Перейдите на [jsfiddle.net](https://jsfiddle.net/greggman/8djzyjL3/) или [jsbin.com](https://jsbin.com) +или [codepen.io](https://codepen.io/greggman/pen/YGQjVV) и просто начните применять уроки здесь. + +На всех из них вы можете ссылаться на внешние скрипты, добавляя пару тегов ``, +если хотите использовать внешние скрипты. + +Тем не менее, есть ограничения. WebGL имеет более строгие ограничения, чем Canvas2D для загрузки изображений, +что означает, что вы не можете легко получить доступ к изображениям со всего интернета для вашей WebGL работы. +Кроме того, просто быстрее работать со всем локально. + +Давайте предположим, что вы хотите запускать и редактировать примеры на этом сайте. Первое, что вы должны +сделать - это скачать сайт. [Вы можете скачать его здесь](https://github.com/gfxfundamentals/webgl2-fundamentals/tree/gh-pages). + +{{{image url="resources/download-webglfundamentals.gif" }}} + +Распакуйте файлы в какую-то папку. + +## Использование маленького простого веб-сервера + +Далее вы должны установить маленький веб-сервер. Я знаю, что "веб-сервер" звучит страшно, но правда в том, что [веб- +серверы на самом деле чрезвычайно просты](https://games.greggman.com/game/saving-and-loading-files-in-a-web-page/). + +Вот очень простой с интерфейсом, называемый [Servez](https://greggman.github.io/servez). + +{{{image url="resources/servez.gif" }}} + +Просто укажите на папку, где вы распаковали файлы, нажмите "Start", затем перейдите +в вашем браузере на [`http://localhost:8080/webgl/`](http://localhost:8080/webgl/) и выберите +пример. + +Если вы предпочитаете командную строку, другой способ - использовать [node.js](https://nodejs.org). +Скачайте его, установите, затем откройте командную строку / консоль / терминальное окно. Если вы на Windows, установщик +добавит специальную "Node Command Prompt", поэтому используйте её. + +Затем установите [`servez`](https://github.com/greggman/servez-cli), набрав + + npm -g install servez + +Если вы на OSX, используйте + + sudo npm -g install servez + +Как только вы это сделали, наберите + + servez path/to/folder/where/you/unzipped/files + +Он должен напечатать что-то вроде + +{{{image url="resources/servez-response.png" }}} + +Затем в вашем браузере перейдите на [`http://localhost:8080/webgl/`](http://localhost:8080/webgl/). + +Если вы не укажете путь, то servez будет обслуживать текущую папку. + +## Использование инструментов разработчика вашего браузера + +Большинство браузеров имеют встроенные обширные инструменты разработчика. + +{{{image url="resources/chrome-devtools.png" }}} + +[Документация для Chrome здесь](https://developers.google.com/web/tools/chrome-devtools/), +[Firefox здесь](https://developer.mozilla.org/en-US/docs/Tools). + +Научитесь их использовать. Если ничего другого, всегда проверяйте JavaScript консоль. Если есть проблема, там часто будет +сообщение об ошибке. Внимательно прочитайте сообщение об ошибке, и вы должны получить подсказку, где проблема. + +{{{image url="resources/javascript-console.gif" }}} + +## WebGL Lint + +[Здесь](https://greggman.github.io/webgl-lint/) есть скрипт для проверки нескольких +ошибок webgl. Просто добавьте это на вашу страницу перед другими скриптами + +``` + +``` + +и ваша программа выбросит исключение, если получит ошибку WebGL, и если вам повезет, +напечатает больше информации. + +[Вы также можете добавить имена к вашим webgl ресурсам](https://github.com/greggman/webgl-lint#naming-your-webgl-objects-buffers-textures-programs-etc) +(буферы, текстуры, шейдеры, программы, ...), так что когда вы получите сообщение об ошибке, оно +будет включать имена ресурсов, относящихся к ошибке. + +## Расширения + +Есть различные WebGL инспекторы. +[Вот один для Chrome и Firefox](https://spector.babylonjs.com/). + +{{{image url="https://camo.githubusercontent.com/5bbc9caf2fc0ecc2eebf615fa8348146b37b08fe/68747470733a2f2f73706563746f72646f632e626162796c6f6e6a732e636f6d2f70696374757265732f7469746c652e706e67" }}} + +Примечание: [ПРОЧИТАЙТЕ ДОКУМЕНТАЦИЮ](https://github.com/BabylonJS/Spector.js/blob/master/readme.md)! + +Версия расширения spector.js захватывает кадры. Что это означает - она работает только +если ваше WebGL приложение успешно инициализирует себя и затем рендерит в +цикле `requestAnimationFrame`. Вы нажимаете кнопку "record", и она захватывает +все вызовы WebGL API для одного "кадра". + +Это означает, что без некоторой работы это не поможет вам найти проблемы во время инициализации. + +Для обхода этого есть 2 метода. + +1. Используйте его как библиотеку, а не как расширение. + + См. [документацию](https://github.com/BabylonJS/Spector.js/blob/master/readme.md). Таким образом вы можете сказать ему "Захвати команды WebGL API сейчас!" + +2. Измените ваше приложение так, чтобы оно не запускалось, пока вы не нажмете кнопку. + + Таким образом вы можете перейти к расширению и выбрать "record", а затем запустить ваше + приложение. Если ваше приложение не анимирует, просто добавьте несколько фальшивых кадров. Пример: + +```html + + +``` + +```js +function main() { + // Получить WebGL контекст + /** @type {HTMLCanvasElement} */ + const canvas = document.querySelector("#canvas"); + const gl = canvas.getContext("webgl"); + if (!gl) { + return; + } + + const startElem = document.querySelector('button'); + startElem.addEventListener('click', start, {once: true}); + + function start() { + // запустить инициализацию в rAF, поскольку spector захватывает только внутри событий rAF + requestAnimationFrame(() => { + // сделать всю инициализацию + init(gl); + }); + // сделать больше кадров, чтобы spector было что смотреть. + requestAnimationFrame(() => {}); + requestAnimationFrame(() => {}); + requestAnimationFrame(() => {}); + } +} + +main(); +``` + +Теперь вы можете нажать "record" в расширении spector.js, затем нажать "start" на вашей странице, +и spector запишет вашу инициализацию. + +Safari также имеет аналогичную встроенную функцию, которая имеет [аналогичные проблемы с аналогичными обходами](https://stackoverflow.com/questions/62446483/debugging-in-webgl). + +Когда я использую такой помощник, я часто нажимаю на вызов рисования и проверяю uniforms. Если я вижу кучу `NaN` (NaN = Not a Number), то я обычно могу отследить код, который установил этот uniform, и найти ошибку. + +## Изучите код + +Также всегда помните, что вы можете изучить код. Вы обычно можете просто выбрать просмотр исходного кода + +{{{image url="resources/view-source.gif" }}} + +Даже если вы не можете щелкнуть правой кнопкой мыши на странице или если исходный код в отдельном файле, +вы всегда можете просмотреть исходный код в devtools + +{{{image url="resources/devtools-source.gif" }}} + +## Начать + +Надеюсь, это поможет вам начать. [Теперь обратно к урокам](index.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-shaders-and-glsl.md b/webgl/lessons/ru/webgl-shaders-and-glsl.md new file mode 100644 index 000000000..de3b66de8 --- /dev/null +++ b/webgl/lessons/ru/webgl-shaders-and-glsl.md @@ -0,0 +1,199 @@ +Title: WebGL2 Шейдеры и GLSL +Description: Что такое шейдер и что такое GLSL +TOC: Шейдеры и GLSL + + +Это продолжение [Основ WebGL](webgl-fundamentals.html). +Если вы не читали о том, как работает WebGL, возможно, вы захотите [сначала прочитать это](webgl-how-it-works.html). + +Мы говорили о шейдерах и GLSL, но не давали им никаких конкретных деталей. +Я думал, что это будет понятно на примерах, но давайте попробуем сделать это яснее на всякий случай. + +Как упоминалось в [как это работает](webgl-how-it-works.html), WebGL требует 2 шейдера каждый раз, когда вы +что-то рисуете. *Вершинный шейдер* и *фрагментный шейдер*. Каждый шейдер - это *функция*. Вершинный +шейдер и фрагментный шейдер связаны вместе в шейдерную программу (или просто программу). Типичное +WebGL приложение будет иметь много шейдерных программ. + +## Вершинный шейдер + +Задача вершинного шейдера - генерировать координаты clip space. Он всегда имеет форму + + #version 300 es + void main() { + gl_Position = doMathToMakeClipspaceCoordinates + } + +Ваш шейдер вызывается один раз для каждой вершины. Каждый раз, когда он вызывается, вы обязаны установить специальную глобальную переменную `gl_Position` в некоторые координаты clip space. + +Вершинным шейдерам нужны данные. Они могут получить эти данные 3 способами. + +1. [Атрибуты](#attributes) (данные, извлеченные из буферов) +2. [Uniforms](#uniforms) (значения, которые остаются одинаковыми для всех вершин одного вызова рисования) +3. [Текстуры](#textures-in-vertex-shaders) (данные из пикселей/текселей) + +### Атрибуты + +Самый распространенный способ для вершинного шейдера получить данные - через буферы и *атрибуты*. +[Как это работает](webgl-how-it-works.html) покрывает буферы и +атрибуты. Вы создаете буферы, + + var buf = gl.createBuffer(); + +помещаете данные в эти буферы + + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW); + +Затем, учитывая шейдерную программу, которую вы создали, вы ищете местоположение ее атрибутов, + + var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position"); + +затем говорите WebGL, как извлекать данные из этих буферов и в атрибут + + // включаем получение данных из буфера для этого атрибута + gl.enableVertexAttribArray(positionLoc); + + var numComponents = 3; // (x, y, z) + var type = gl.FLOAT; + var normalize = false; // оставляем значения как есть + var offset = 0; // начинаем с начала буфера + var stride = 0; // сколько байт переместиться к следующей вершине + // 0 = использовать правильный stride для type и numComponents + + gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset); + +В [Основах WebGL](webgl-fundamentals.html) мы показали, что мы можем не делать математику +в шейдере и просто передавать данные напрямую. + + #version 300 es + + in vec4 a_position; + + void main() { + gl_Position = a_position; + } + +Если мы поместим вершины clip space в наши буферы, это будет работать. + +Атрибуты могут использовать `float`, `vec2`, `vec3`, `vec4`, `mat2`, `mat3`, `mat4`, +`int`, `ivec2`, `ivec3`, `ivec4`, `uint`, `uvec2`, `uvec3`, `uvec4` как типы. + +### Uniforms + +Для вершинного шейдера uniforms - это значения, передаваемые в вершинный шейдер, которые остаются одинаковыми +для всех вершин в вызове рисования. Как очень простой пример, мы могли бы добавить смещение к +вершинному шейдеру выше + + #version 300 es + + in vec4 a_position; + +uniform vec4 u_offset; + + void main() { + gl_Position = a_position + u_offset; + } + +И теперь мы могли бы сместить каждую вершину на определенное количество. Сначала мы бы нашли +местоположение uniform + + var offsetLoc = gl.getUniformLocation(someProgram, "u_offset"); + +И затем перед рисованием мы бы установили uniform + + gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // смещаем вправо на половину экрана + +Uniforms могут быть многих типов. Для каждого типа вы должны вызвать соответствующую функцию для его установки. + + gl.uniform1f (floatUniformLoc, v); // для float + gl.uniform1fv(floatUniformLoc, [v]); // для float или массива float + gl.uniform2f (vec2UniformLoc, v0, v1); // для vec2 + gl.uniform2fv(vec2UniformLoc, [v0, v1]); // для vec2 или массива vec2 + gl.uniform3f (vec3UniformLoc, v0, v1, v2); // для vec3 + gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // для vec3 или массива vec3 + gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // для vec4 + gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // для vec4 или массива vec4 + + gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // для mat2 или массива mat2 + gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // для mat3 или массива mat3 + gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // для mat4 или массива mat4 + + gl.uniform1i (intUniformLoc, v); // для int + gl.uniform1iv(intUniformLoc, [v]); // для int или массива int + gl.uniform2i (ivec2UniformLoc, v0, v1); // для ivec2 + gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // для ivec2 или массива ivec2 + gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // для ivec3 + gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // для ivec3 или массива ivec3 + gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // для ivec4 + gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // для ivec4 или массива ivec4 + + gl.uniform1u (intUniformLoc, v); // для uint + gl.uniform1uv(intUniformLoc, [v]); // для uint или массива uint + gl.uniform2u (ivec2UniformLoc, v0, v1); // для uvec2 + gl.uniform2uv(ivec2UniformLoc, [v0, v1]); // для uvec2 или массива uvec2 + gl.uniform3u (ivec3UniformLoc, v0, v1, v2); // для uvec3 + gl.uniform3uv(ivec3UniformLoc, [v0, v1, v2]); // для uvec3 или массива uvec3 + gl.uniform4u (ivec4UniformLoc, v0, v1, v2, v4); // для uvec4 + gl.uniform4uv(ivec4UniformLoc, [v0, v1, v2, v4]); // для uvec4 или массива uvec4 + + // для sampler2D, sampler3D, samplerCube, samplerCubeShadow, sampler2DShadow, + // sampler2DArray, sampler2DArrayShadow + gl.uniform1i (samplerUniformLoc, v); + gl.uniform1iv(samplerUniformLoc, [v]); + +Есть также типы `bool`, `bvec2`, `bvec3`, и `bvec4`. Они используют либо функции `gl.uniform?f?`, `gl.uniform?i?`, +или `gl.uniform?u?`. + +Обратите внимание, что для массива вы можете установить все uniforms массива сразу. Например + + // в шейдере + uniform vec2 u_someVec2[3]; + + // в JavaScript во время инициализации + var someVec2Loc = gl.getUniformLocation(someProgram, "u_someVec2"); + + // во время рендеринга + gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]); // установить весь массив u_someVec2 + +Но если вы хотите установить отдельные элементы массива, вы должны найти местоположение +каждого элемента отдельно. + + // в JavaScript во время инициализации + var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]"); + var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]"); + var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]"); + + // во время рендеринга + gl.uniform2fv(someVec2Element0Loc, [1, 2]); // установить элемент 0 + gl.uniform2fv(someVec2Element1Loc, [3, 4]); // установить элемент 1 + gl.uniform2fv(someVec2Element2Loc, [5, 6]); // установить элемент 2 + +Аналогично, если вы создаете структуру + + struct SomeStruct { + bool active; + vec2 someVec2; + }; + uniform SomeStruct u_someThing; + +вы должны найти каждое поле отдельно + + var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active"); + var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2"); + +### Текстуры в вершинных шейдерах + +См. [Текстуры в фрагментных шейдерах](#textures-in-fragment-shaders). + +## Фрагментный шейдер + +Задача фрагментного шейдера - предоставить цвет для текущего пикселя, который растеризуется. +Он всегда имеет форму + + #version 300 es + precision highp float; + + out vec4 outColor; // вы можете выбрать любое имя + + void main() { + outColor = doMathToMakeAColor; + } \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-shadertoy.md b/webgl/lessons/ru/webgl-shadertoy.md new file mode 100644 index 000000000..9afb20b77 --- /dev/null +++ b/webgl/lessons/ru/webgl-shadertoy.md @@ -0,0 +1,530 @@ +Title: WebGL2 Shadertoy +Description: Шейдеры Shadertoy +TOC: Shadertoy + +Эта статья предполагает, что вы прочитали многие другие статьи, +начиная с [основ](webgl-fundamentals.html). +Если вы их не читали, пожалуйста, начните сначала там. + +В [статье о рисовании без данных](webgl-drawing-without-data.html) +мы показали несколько примеров рисования вещей без данных, используя +вершинный шейдер. Эта статья будет о рисовании вещей без +данных, используя фрагментные шейдеры. + +Мы начнем с простого шейдера сплошного цвета +без математики, используя код [из самой первой статьи](webgl-fundamentals.html). + +Простой вершинный шейдер + +```js +const vs = `#version 300 es + // атрибут - это вход (in) в вершинный шейдер. + // Он будет получать данные из буфера + in vec4 a_position; + + // все шейдеры имеют главную функцию + void main() { + + // gl_Position - это специальная переменная, за установку которой + // отвечает вершинный шейдер + gl_Position = a_position; + } +`; +``` + +и простой фрагментный шейдер + +```js +const fs = `#version 300 es + precision highp float; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + outColor = vec4(1, 0, 0.5, 1); // возвращаем красно-фиолетовый + } +`; +``` + +Затем нам нужно скомпилировать и связать шейдеры и найти местоположение атрибута position. + +```js +function main() { + // Получаем WebGL контекст + /** @type {HTMLCanvasElement} */ + const canvas = document.querySelector("#canvas"); + const gl = canvas.getContext("webgl2"); + if (!gl) { + return; + } + + // настройка GLSL программы + const program = webglUtils.createProgramFromSources(gl, [vs, fs]); + + // ищем, куда должны идти вершинные данные. + const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); +``` + +и затем создать вершинный массив, +заполнить буфер 2 треугольниками, которые создают прямоугольник в clip space, который +идет от -1 до +1 по x и y, чтобы покрыть canvas, и установить атрибуты. + +```js + // Создаем объект вершинного массива (состояние атрибутов) + const vao = gl.createVertexArray(); + + // и делаем его тем, с которым мы сейчас работаем + gl.bindVertexArray(vao); + + // Создаем буфер для размещения трех 2d точек clip space + const positionBuffer = gl.createBuffer(); + + // Привязываем его к ARRAY_BUFFER (думайте об этом как ARRAY_BUFFER = positionBuffer) + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + + // заполняем его 2 треугольниками, которые покрывают clip space + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, -1, // первый треугольник + 1, -1, + -1, 1, + -1, 1, // второй треугольник + 1, -1, + 1, 1, + ]), gl.STATIC_DRAW); + + // Включаем атрибут + gl.enableVertexAttribArray(positionAttributeLocation); + + // Говорим атрибуту, как получать данные из positionBuffer (ARRAY_BUFFER) + gl.vertexAttribPointer( + positionAttributeLocation, + 2, // 2 компонента на итерацию + gl.FLOAT, // данные - 32-битные float'ы + false, // не нормализуем данные + 0, // 0 = двигаемся вперед на size * sizeof(type) каждую итерацию, чтобы получить следующую позицию + 0, // начинаем с начала буфера + ); +``` + +И затем мы рисуем + +```js + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + // Говорим WebGL, как конвертировать из clip space в пиксели + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Говорим использовать нашу программу (пару шейдеров) + gl.useProgram(program); + + // Привязываем набор атрибутов/буферов, который мы хотим. + gl.bindVertexArray(vao); + + gl.drawArrays( + gl.TRIANGLES, + 0, // смещение + 6, // количество вершин для обработки + ); +``` + +И конечно, мы получаем сплошной цвет, который покрывает canvas. + +{{{example url="../webgl-shadertoy-solid.html"}}} + +В [статье о том, как работает WebGL](webgl-how-it-works.html) мы добавили больше +цвета, предоставляя цвет для каждой вершины. В [статье о текстурах](webgl-3d-textures.html) +мы добавили больше цвета, предоставляя текстуры и координаты текстуры. +Так как же мы получаем что-то большее, чем сплошной цвет, без дополнительных данных? +WebGL предоставляет переменную, называемую `gl_FragCoord`, которая равна **пиксельной** +координате пикселя, который в данный момент рисуется. + +Итак, давайте изменим наш фрагментный шейдер, чтобы использовать это для вычисления цвета + +```js +const fs = `#version 300 es + precision highp float; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + outColor = vec4(fract(gl_FragCoord.xy / 50.0), 0, 1); + } +`; +``` + +Как мы упомянули выше, `gl_FragCoord` - это **пиксельная** координата, поэтому она будет +считаться поперек и вверх canvas. Разделив на 50, мы получим значение, которое идет +от 0 до 1, когда `gl_FragCoord` идет от 0 до 50. Используя `fract`, мы +сохраним только *дробную* часть, так что, например, когда `gl_FragCoord` равен 75. +75 / 50 = 1.5, fract(1.5) = 0.5, поэтому мы получим значение, которое идет от 0 до 1 +каждые 50 пикселей. + +{{{example url="../webgl-shadertoy-gl-fragcoord.html"}}} + +Как вы можете видеть выше, каждые 50 пикселей поперек красный идет от 0 до 1, +и каждые 50 пикселей вверх зеленый идет от 0 до 1. + +С нашей настройкой теперь мы могли бы сделать более сложную математику для более причудливого изображения. +но у нас есть одна проблема в том, что мы не знаем, насколько велик canvas, +поэтому нам пришлось бы жестко кодировать для конкретного размера. Мы можем решить эту проблему, +передав размер canvas, а затем разделив `gl_FragCoord` на +размер, чтобы дать нам значение, которое идет от 0 до 1 поперек и вверх canvas +независимо от размера. + +```js +const fs = `#version 300 es + precision highp float; + + uniform vec2 u_resolution; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + outColor = vec4(fract(gl_FragCoord.xy / u_resolution), 0, 1); + } +`; +``` + +и найти и установить uniform + +```js +// ищем, куда должны идти вершинные данные. +const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); + +// ищем местоположения uniforms +const resolutionLocation = gl.getUniformLocation(program, "u_resolution"); +``` + +и установить uniform + +```js +gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); +``` + +что позволяет нам сделать наш разброс красного и зеленого всегда подходящим для canvas независимо +от разрешения + +{{{example url="../webgl-shadertoy-w-resolution.html"}}} + +Давайте также передадим позицию мыши в пиксельных координатах. + +```js +const fs = `#version 300 es + precision highp float; + + uniform vec2 u_resolution; + uniform vec2 u_mouse; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + outColor = vec4(fract((gl_FragCoord.xy - u_mouse) / u_resolution), 0, 1); + } +`; +``` + +И затем нам нужно найти местоположение uniform, + +```js +// ищем местоположения uniforms +const resolutionLocation = gl.getUniformLocation(program, "u_resolution"); +const mouseLocation = gl.getUniformLocation(program, "u_mouse"); +``` + +отслеживать мышь, + +```js +let mouseX = 0; +let mouseY = 0; + +function setMousePosition(e) { + const rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.left; + mouseY = rect.height - (e.clientY - rect.top) - 1; // низ равен 0 в WebGL + render(); +} + +canvas.addEventListener('mousemove', setMousePosition); +``` + +и установить uniform. + +```js +gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); +gl.uniform2f(mouseLocation, mouseX, mouseY); +``` + +Нам также нужно изменить код, чтобы мы рендерили, когда позиция мыши изменяется + +```js +function setMousePosition(e) { + const rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.left; + mouseY = rect.height - (e.clientY - rect.top) - 1; // низ равен 0 в WebGL + render(); +} + +function render() { + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + ... + + gl.drawArrays( + gl.TRIANGLES, + 0, // смещение + 6, // количество вершин для обработки + ); +} +render(); +``` + +и пока мы этим занимаемся, давайте также обработаем касание + +```js +canvas.addEventListener('mousemove', setMousePosition); +canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); +}, {passive: false}); +canvas.addEventListener('touchmove', (e) => { + e.preventDefault(); + setMousePosition(e.touches[0]); +}, {passive: false}); +``` + +и теперь вы можете видеть, что если вы двигаете мышь над примером, это влияет на наше изображение. + +{{{example url="../webgl-shadertoy-w-mouse.html"}}} + +Финальная основная часть - мы хотим иметь возможность анимировать что-то, поэтому мы передаем еще одну +вещь, значение времени, которое мы можем использовать для добавления к нашим вычислениям. + +Например, если бы мы сделали это + +```js +const fs = `#version 300 es + precision highp float; + + uniform vec2 u_resolution; + uniform vec2 u_mouse; + uniform float u_time; + + // нам нужно объявить выход для фрагментного шейдера + out vec4 outColor; + + void main() { + outColor = vec4(fract((gl_FragCoord.xy - u_mouse) / u_resolution), fract(u_time), 1); + } +`; +``` + +И теперь синий канал будет пульсировать в такт времени. Нам просто нужно +найти uniform и установить его в цикле [requestAnimationFrame](webgl-animation.html). + +```js +// ищем местоположения uniforms +const resolutionLocation = gl.getUniformLocation(program, "u_resolution"); +const mouseLocation = gl.getUniformLocation(program, "u_mouse"); +const timeLocation = gl.getUniformLocation(program, "u_time"); + +... + +function render(time) { + time *= 0.001; // конвертируем в секунды + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + ... + + gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); + gl.uniform2f(mouseLocation, mouseX, mouseY); + gl.uniform1f(timeLocation, time); + + gl.drawArrays( + gl.TRIANGLES, + 0, // смещение + 6, // количество вершин для обработки + ); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +Также нам больше не нужно рендерить при движении мыши, поскольку мы рендерим непрерывно. + +```js +let mouseX = 0; +let mouseY = 0; +canvas.addEventListener('mousemove', (e) => { + const rect = canvas.getBoundingClientRect(); + mouseX = e.clientX - rect.left; + mouseY = rect.height - (e.clientY - rect.top) - 1; // низ равен 0 в WebGL +}); +``` + +И мы получаем простую, но скучную анимацию. + +{{{example url="../webgl-shadertoy-w-time.html"}}} + +Итак, теперь со всем этим мы можем взять шейдер с [Shadertoy.com](https://shadertoy.com). В шейдерах Shadertoy вы предоставляете функцию, называемую `mainImage`, в этой форме + +```glsl +void mainImage(out vec4 fragColor, in vec2 fragCoord) +{ +} +``` + +Где ваша задача - установить `fragColor` так же, как вы обычно устанавливали бы `gl_FragColor`, и +`fragCoord` - это то же самое, что и `gl_FragCoord`. Добавление этой дополнительной функции позволяет Shadertoy +наложить немного больше структуры, а также выполнить некоторую дополнительную работу до или после вызова +`mainImage`. Для нас, чтобы использовать это, нам просто нужно вызвать это так + +```glsl +#version 300 es +precision highp float; + +uniform vec2 u_resolution; +uniform vec2 u_mouse; +uniform float u_time; + +out vec4 outColor; + +//---вставьте код shadertoy здесь-- + +void main() { + mainImage(outColor, gl_FragCoord.xy); +} +``` + +За исключением того, что Shadertoy использует имена uniforms `iResolution`, `iMouse` и `iTime`, поэтому давайте переименуем их. + +```glsl +#version 300 es +precision highp float; + +uniform vec2 iResolution; +uniform vec2 iMouse; +uniform float iTime; + +//---вставьте код shadertoy здесь-- + +out vec4 outColor; + +void main() { + mainImage(outColor, gl_FragCoord.xy); +} +``` + +и найти их по новым именам + +```js +// ищем местоположения uniforms +const resolutionLocation = gl.getUniformLocation(program, "iResolution"); +const mouseLocation = gl.getUniformLocation(program, "iMouse"); +const timeLocation = gl.getUniformLocation(program, "iTime"); +``` + +Взяв [этот шейдер shadertoy](https://www.shadertoy.com/view/3l23Rh) и вставив его +в наш шейдер выше, где написано `//---вставьте код shadertoy здесь--`, мы получаем... + +{{{example url="../webgl-shadertoy.html"}}} + +Это необычайно красивое изображение для отсутствия данных! + +Я сделал пример выше рендериться только когда мышь находится над canvas или когда касается. +Это потому, что математика, необходимая +для рисования изображения выше, сложна и медленна, и позволить ей работать непрерывно +сделало бы очень трудным взаимодействие с этой страницей. Если у вас +очень быстрый GPU, изображение выше может работать плавно. На моем ноутбуке +хотя оно работает медленно и рывками. + +Это поднимает чрезвычайно важный момент. **Шейдеры на +shadertoy не являются лучшей практикой**. Shadertoy - это головоломка и +вызов *"Если у меня нет данных и только функция, которая +принимает очень мало входных данных, могу ли я сделать интересное или красивое +изображение"*. Это не способ сделать производительный WebGL. + +Возьмите, например, [этот удивительный шейдер shadertoy](https://www.shadertoy.com/view/4sS3zG), который выглядит так + +
+ +Он красивый, но работает со скоростью около 19 кадров в секунду в крошечном +окне 640x360 на моем мощном ноутбуке. Расширьте окно до полного экрана, и оно работает около +2 или 3 кадров в секунду. Тестирование на моем более мощном настольном компьютере оно все еще достигает только 45 кадров в +секунду при 640x360 и может быть 10 в полноэкранном режиме. + +Сравните это с этой игрой, которая также довольно красива и все же работает со скоростью 30-60 кадров в секунду +даже на менее мощных GPU + + + +Это потому, что игра использует лучшие практики, рисуя вещи текстурированными +треугольниками вместо сложной математики. + +Итак, пожалуйста, примите это близко к сердцу. Примеры на Shadertoy +просто удивительны отчасти потому, что теперь вы знаете, что они сделаны +под экстремальным ограничением почти отсутствия данных и являются сложными +функциями, которые рисуют красивые картины. Как таковые, они являются предметом +удивления. + +Они также отличный способ изучить много математики. +Но они также никоим образом не являются способом получить производительное +WebGL приложение. Поэтому, пожалуйста, имейте это в виду. + +В противном случае, если вы хотите запустить больше шейдеров Shadertoy, вам +потребуется предоставить еще несколько uniforms. Вот список +uniforms, которые предоставляет Shadertoy + +
+ + + + + + + + + + + + + +
типимягдеописание
vec3iResolutionimage / bufferРазрешение viewport (z - соотношение сторон пикселя, обычно 1.0)
floatiTimeimage / sound / bufferТекущее время в секундах
floatiTimeDeltaimage / bufferВремя, необходимое для рендеринга кадра, в секундах
intiFrameimage / bufferТекущий кадр
floatiFrameRateimage / bufferКоличество кадров, рендеренных в секунду
floatiChannelTime[4]image / bufferВремя для канала (если видео или звук), в секундах
vec3iChannelResolution[4]image / buffer / soundРазрешение входной текстуры для каждого канала
vec4iMouseimage / bufferxy = текущие пиксельные координаты (если LMB нажата). zw = координаты клика
sampler2DiChannel{i}image / buffer / soundСэмплер для входных текстур i
vec4iDateimage / buffer / soundГод, месяц, день, время в секундах в .xyzw
floatiSampleRateimage / buffer / soundЧастота дискретизации звука (обычно 44100)
+ +Обратите внимание, что `iMouse` и `iResolution` на самом деле должны быть +`vec4` и `vec3` соответственно, поэтому вам может потребоваться настроить +их, чтобы они соответствовали. + +`iChannel` - это текстуры, поэтому если шейдер нуждается в них, вам нужно будет +предоставить [текстуры](webgl-3d-textures.html). + +Shadertoy также позволяет вам использовать несколько шейдеров для рендеринга в +текстуры вне экрана, поэтому если шейдер нуждается в них, вам нужно будет настроить +[текстуры для рендеринга](webgl-render-to-texture.html). + +Колонка "где" указывает, какие uniforms +доступны в каких шейдерах. "image" - это шейдер, +который рендерит в canvas. "buffer" - это шейдер, +который рендерит в текстуру вне экрана. "sound" - это +шейдер, где [ожидается, что ваш шейдер будет генерировать +звуковые данные в текстуру](https://stackoverflow.com/questions/34859701/how-do-shadertoys-audio-shaders-work). + +Я надеюсь, это помогло объяснить Shadertoy. Это отличный сайт с удивительными работами, +но хорошо знать, что на самом деле происходит. Если вы хотите узнать больше о +техниках, используемых в этих видах шейдеров, 2 хороших ресурса - +[блог человека, который создал сайт shadertoy]("https://www.iquilezles.org/www/index.htm) и [The Book of Shaders](https://thebookofshaders.com/) (что немного вводит в заблуждение, поскольку на самом деле покрывает только виды шейдеров, используемых на shadertoy, а не виды, используемые в производительных приложениях и играх. Тем не менее, это отличный ресурс! + +
+

Пиксельные координаты

+

Пиксельные координаты в WebGL +ссылаются на их края. Так, например, если бы у нас был canvas размером 3x2 пикселя, то +значение для gl_FragCoord в пикселе 2 +слева и 1 снизу +было бы 2.5, 1.5 +

+
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-shadows-continued.md b/webgl/lessons/ru/webgl-shadows-continued.md new file mode 100644 index 000000000..bc83f4e43 --- /dev/null +++ b/webgl/lessons/ru/webgl-shadows-continued.md @@ -0,0 +1,5 @@ +Title: WebGL2 - Тени (продолжение) +Description: Как вычислять тени (продолжение) +TOC: Тени (продолжение) + +В разработке \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-shadows.md b/webgl/lessons/ru/webgl-shadows.md new file mode 100644 index 000000000..9a3ca57bb --- /dev/null +++ b/webgl/lessons/ru/webgl-shadows.md @@ -0,0 +1,199 @@ +Title: WebGL2 Тени +Description: Как вычислять тени +TOC: Тени + +Давайте нарисуем некоторые тени! + +## Предварительные требования + +Вычисление базовых теней не *так* сложно, но требует +много фоновых знаний. Чтобы понять эту статью, +вам нужно уже понимать следующие темы. + +* [Ортографическая проекция](webgl-3d-orthographic.html) +* [Перспективная проекция](webgl-3d-perspective.html) +* [Прожекторное освещение](webgl-3d-lighting-spot.html) +* [Текстуры](webgl-3d-textures.html) +* [Рендеринг в текстуру](webgl-render-to-texture.html) +* [Проекция текстур](webgl-planar-projection-mapping.html) +* [Визуализация камеры](webgl-visualizing-the-camera.html) + +Поэтому, если вы их не читали, пожалуйста, сначала прочитайте их. + +Помимо этого, эта статья предполагает, что вы прочитали статью о +[меньше кода больше веселья](webgl-less-code-more-fun.html), +поскольку она использует библиотеку, упомянутую там, чтобы +не загромождать пример. Если вы не понимаете, +что такое буферы, массивы вершин и атрибуты, или когда +функция называется `twgl.setUniforms`, что означает +установка uniforms и т.д., то вам, вероятно, стоит пойти дальше назад и +[прочитать основы](webgl-fundamentals.html). + +Итак, во-первых, есть более одного способа рисовать тени. +Каждый способ имеет свои компромиссы. Самый распространенный способ рисовать +тени - использовать карты теней. + +Карты теней работают, комбинируя техники из всех предварительных +статей выше. + +В [статье о проекционном маппинге](webgl-planar-projection-mapping.html) +мы видели, как проецировать изображение на объекты + +{{{example url="../webgl-planar-projection-with-projection-matrix.html"}}} + +Напомним, что мы не рисовали это изображение поверх объектов в сцене, +скорее, когда объекты рендерились, для каждого пикселя мы проверяли, находится ли +проецируемая текстура в диапазоне, если да, то мы брали соответствующий цвет из +проецируемой текстуры, если нет, то мы брали цвет из другой текстуры, +цвет которой искался с использованием координат текстуры, которые маппили текстуру +на объект. + +Что, если проецируемая текстура вместо этого содержала данные глубины с точки +зрения источника света. Другими словами, предположим, что был источник света на кончике +усеченной пирамиды, показанной в том примере выше, и проецируемая текстура имела информацию о глубине +с точки зрения источника света. Сфера имела бы значения глубины ближе +к источнику света, плоскость имела бы значения глубины дальше +от источника света. + +
+ +Если бы у нас были эти данные, то при выборе цвета для рендеринга +мы могли бы получить значение глубины из проецируемой текстуры и проверить, является ли +глубина пикселя, который мы собираемся нарисовать, ближе или дальше от источника света. +Если она дальше от источника света, это означает, что что-то еще было ближе к источнику света. Другими словами, +что-то блокирует свет, поэтому этот пиксель в тени. + +
+ +Здесь текстура глубины проецируется через пространство света внутри усеченной пирамиды с точки зрения источника света. +Когда мы рисуем пиксели пола, мы вычисляем глубину этого пикселя с точки зрения +источника света (0.3 на диаграмме выше). Затем мы смотрим на соответствующую глубину в +проецируемой карте глубины. С точки зрения источника света значение глубины +в текстуре будет 0.1, потому что она попала в сферу. Видя, что 0.1 < 0.3, мы +знаем, что пол в этой позиции должен быть в тени. + +Сначала давайте нарисуем карту теней. Мы возьмем последний пример из +[статьи о проекционном маппинге](webgl-planar-projection-mapping.html), +но вместо загрузки текстуры мы будем [рендерить в текстуру](webgl-render-to-texture.html), +поэтому мы создаем текстуру глубины и прикрепляем ее к framebuffer как `DEPTH_ATTACHMENT`. + +```js +const depthTexture = gl.createTexture(); +const depthTextureSize = 512; +gl.bindTexture(gl.TEXTURE_2D, depthTexture); +gl.texImage2D( + gl.TEXTURE_2D, // target + 0, // mip level + gl.DEPTH_COMPONENT32F, // internal format + depthTextureSize, // width + depthTextureSize, // height + 0, // border + gl.DEPTH_COMPONENT, // format + gl.FLOAT, // type + null); // data +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + +const depthFramebuffer = gl.createFramebuffer(); +gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); +gl.framebufferTexture2D( + gl.FRAMEBUFFER, // target + gl.DEPTH_ATTACHMENT, // attachment point + gl.TEXTURE_2D, // texture target + depthTexture, // texture + 0); // mip level +``` + +Чтобы использовать это, нам нужно уметь рендерить сцену более одного раза с разными +шейдерами. Один раз с простым шейдером только для рендеринга в текстуру глубины, а +затем снова с нашим текущим шейдером, который проецирует текстуру. + +Итак, сначала давайте изменим `drawScene`, чтобы мы могли передать ей программу, с которой хотим +рендерить + +```js +-function drawScene(projectionMatrix, cameraMatrix, textureMatrix) { ++function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo) { + // Создаем матрицу вида из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix); + +- gl.useProgram(textureProgramInfo.program); ++ gl.useProgram(programInfo.program); + + // устанавливаем uniforms, которые одинаковы для сферы и плоскости + // примечание: любые значения без соответствующего uniform в шейдере + // игнорируются. +- twgl.setUniforms(textureProgramInfo, { ++ twgl.setUniforms(programInfo, { + u_view: viewMatrix, + u_projection: projectionMatrix, +* u_textureMatrix: textureMatrix, +- u_projectedTexture: imageTexture, ++ u_projectedTexture: depthTexture, + }); + + // ------ Рисуем сферу -------- + + // Настраиваем все необходимые атрибуты. + gl.bindVertexArray(sphereVAO); + + // Устанавливаем uniforms, уникальные для сферы +- twgl.setUniforms(textureProgramInfo, sphereUniforms); ++ twgl.setUniforms(programInfo, sphereUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, sphereBufferInfo); + + // ------ Рисуем плоскость -------- + + // Настраиваем все необходимые атрибуты. + gl.bindVertexArray(planeVAO); + + // Устанавливаем uniforms, которые мы только что вычислили +- twgl.setUniforms(textureProgramInfo, planeUniforms); ++ twgl.setUniforms(programInfo, planeUniforms); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, planeBufferInfo); +} +``` + +Теперь, когда мы собираемся использовать одни и те же массивы вершин с несколькими +программами шейдеров, нам нужно убедиться, что эти программы используют одни и те же атрибуты. +Это было упомянуто ранее при разговоре о массивах вершин (VAO в коде выше), +но я думаю, что это первый пример на этом сайте, который действительно сталкивается с этой +проблемой. Другими словами, мы собираемся рисовать сферу и плоскость как с +программой шейдера проецируемой текстуры, так и с программой шейдера сплошного цвета. +Программа шейдера проецируемой текстуры имеет 2 атрибута, `a_position` и +`a_texcoord`. Программа шейдера сплошного цвета имеет только один, `a_position`. +Если мы не скажем WebGL, какие местоположения атрибутов использовать, возможно, +он установит `a_position` местоположение = 0 для одного шейдера и местоположение = 1 для другого +(или действительно WebGL может выбрать любое произвольное местоположение). Если это произойдет, +то атрибуты, которые мы настроили в `sphereVAO` и `planeVAO`, не будут соответствовать +обеим программам. + +Мы можем решить это 2 способами. + +1. В GLSL добавить `layout(location = 0)` перед каждым атрибутом + + ```glsl + layout(location = 0) in vec4 a_position; + layout(location = 1) in vec4 a_texcoord; + ``` + + Если бы у нас было 150 шейдеров, нам пришлось бы повторять эти местоположения во всех из них + и отслеживать, какие шейдеры используют какие местоположения + +2. вызвать `gl.bindAttribLocation` перед связыванием шейдеров + + В данном случае перед тем, как мы вызовем `gl.linkProgram`, мы вызовем `gl.bindAttribLocation`. + (см. [первую статью](webgl-fundamentals.html)) + + ```js + gl.bindAttribLocation(program, 0, "a_position"); + gl.bindAttribLocation(program, 1, "a_texcoord"); + gl.linkProgram(program); + ... + ``` \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-skinning.md b/webgl/lessons/ru/webgl-skinning.md new file mode 100644 index 000000000..a6924be44 --- /dev/null +++ b/webgl/lessons/ru/webgl-skinning.md @@ -0,0 +1,198 @@ +Title: WebGL2 - Скининг +Description: Как выполнить скининг меша в WebGL +TOC: Скининг + + +Скининг в графике - это название, данное перемещению набора вершин на основе +взвешенного влияния множественных матриц. Это довольно абстрактно. + +Это называется *скинингом*, потому что он обычно используется для создания 3D персонажей +с "скелетом", сделанным из "костей", где "кость" - это другое название для матрицы, +и затем **для каждой вершины** устанавливается влияние каждой кости на эту вершину. + +Так, например, кость руки будет иметь почти 100% влияние на вершины +около руки персонажа, тогда как кость стопы будет иметь нулевое влияние +на те же вершины. Вершины вокруг запястья будут иметь некоторое влияние от кости руки и также некоторое от кости руки. + +Основная часть заключается в том, что вам нужны кости (что является просто причудливым способом сказать +иерархию матриц) и веса. Веса - это значения для каждой вершины, которые идут +от 0 до 1, чтобы сказать, насколько конкретная кость-матрица влияет на позицию +этой вершины. Веса чем-то похожи на цвета вершин с точки зрения данных. +Один набор весов на вершину. Другими словами, веса помещаются в +буфер и предоставляются через атрибуты. + +Обычно вы ограничиваете количество весов на вершину отчасти потому, что +иначе это было бы слишком много данных. Персонаж может иметь откуда угодно +от 15 костей (Virtua Fighter 1) до 150-300 костей (некоторые современные игры). +Если бы у вас было 300 костей, вам понадобилось бы 300 весов НА вершину НА кость. Если бы ваш +персонаж имел 10000 вершин, это потребовало бы 3 миллиона весов. + +Итак, вместо этого большинство систем скиннинга в реальном времени ограничивают это ~4 весами на вершину. +Обычно это достигается в экспортере/конвертере, который берет данные из +3D пакетов, таких как blender/maya/3dsmax, и для каждой вершины находит 4 +кости с наивысшими весами, а затем нормализует эти веса + +Чтобы дать псевдо-пример, не-скинированная вершина обычно вычисляется так + + gl_Position = projection * view * model * position; + +Скинированная вершина эффективно вычисляется так + + gl_Position = projection * view * + (bone1Matrix * position * weight1 + + bone2Matrix * position * weight2 + + bone3Matrix * position * weight3 + + bone4Matrix * position * weight4); + +Как вы можете видеть, это как если бы мы вычисляли 4 разные позиции для каждой вершины, а затем смешивали их обратно в одну, применяя веса. + +Предполагая, что вы сохранили матрицы костей в uniform массиве, и вы +передали веса и к какой кости применяется каждый вес как +атрибуты, вы могли бы сделать что-то вроде + + #version 300 es + in vec4 a_position; + in vec4 a_weights; // 4 веса на вершину + in uvec4 a_boneNdx; // 4 индекса костей на вершину + uniform mat4 bones[MAX_BONES]; // 1 матрица на кость + + gl_Position = projection * view * + (a_bones[a_boneNdx[0]] * a_position * a_weight[0] + + a_bones[a_boneNdx[1]] * a_position * a_weight[1] + + a_bones[a_boneNdx[2]] * a_position * a_weight[2] + + a_boneS[a_boneNdx[3]] * a_position * a_weight[3]); + + +Есть еще одна проблема. Допустим, у вас есть модель человека с +началом координат (0,0,0) на полу прямо между их ногами. + +
+ +Теперь представьте, что вы поместили матрицу/кость/сустав у их головы, и вы хотите использовать +эту кость для скиннинга. Чтобы держать это простым, представьте, что вы просто установили +веса так, что вершины головы имеют вес 1.0 для кости головы, и никакие другие суставы не влияют на эти вершины. + +
+ +Есть проблема. +Вершины головы находятся на 2 единицы выше начала координат. Кость головы также на 2 +единицы выше начала координат. Если бы вы фактически умножили эти вершины головы на +матрицу кости головы, вы получили бы вершины на 4 единицы выше начала координат. Оригинальные +2 единицы вершин + 2 единицы матрицы кости головы. + +
+ +Решение заключается в сохранении "привязочной позы", которая является дополнительной матрицей на сустав +того, где каждая матрица была до того, как вы использовали ее для влияния на вершины. В этом +случае привязочная поза матрицы головы была бы на 2 единицы выше начала координат. +Итак, теперь вы можете использовать обратную матрицу, чтобы вычесть дополнительные 2 +единицы. + +Другими словами, матрицы костей, переданные в шейдер, каждая была +умножена на их обратную привязочную позу, чтобы сделать их влияние только +настолько, насколько они изменились от своих оригинальных позиций относительно начала координат +меша. + +Давайте создадим небольшой пример. Мы будем анимировать в 2d сетку, подобную этой + +
+ +* Где `b0`, `b1` и `b2` - это матрицы костей. +* `b1` является дочерним элементом `b0`, а `b2` является дочерним элементом `b1` +* Вершины `0,1` получат вес 1.0 от кости b0 +* Вершины `2,3` получат вес 0.5 от костей b0 и b1 +* Вершины `4,5` получат вес 1.0 от кости b1 +* Вершины `6,7` получат вес 0.5 от костей b1 и b2 +* Вершины `8,9` получат вес 1.0 от кости b2 + +Мы будем использовать утилиты, описанные в [меньше кода больше веселья](webgl-less-code-more-fun.html). + +Сначала нам нужны вершины и для каждой вершины индекс +каждой кости, которая влияет на нее, и число от 0 до 1 +того, насколько сильно влияет эта кость. + +``` +const arrays = { + position: { + numComponents: 2, + data: [ + 0, 1, // 0 + 0, -1, // 1 + 2, 1, // 2 + 2, -1, // 3 + 4, 1, // 4 + 4, -1, // 5 + 6, 1, // 6 + 6, -1, // 7 + 8, 1, // 8 + 8, -1, // 9 + ], + }, + boneNdx: { + numComponents: 4, + data: new Uint8Array([ + 0, 0, 0, 0, // 0 + 0, 0, 0, 0, // 1 + 0, 1, 0, 0, // 2 + 0, 1, 0, 0, // 3 + 1, 0, 0, 0, // 4 + 1, 0, 0, 0, // 5 + 1, 2, 0, 0, // 6 + 1, 2, 0, 0, // 7 + 2, 0, 0, 0, // 8 + 2, 0, 0, 0, // 9 + ]), + }, + weight: { + numComponents: 4, + data: [ + 1, 0, 0, 0, // 0 + 1, 0, 0, 0, // 1 + .5,.5, 0, 0, // 2 + .5,.5, 0, 0, // 3 + 1, 0, 0, 0, // 4 + 1, 0, 0, 0, // 5 + .5,.5, 0, 0, // 6 + .5,.5, 0, 0, // 7 + 1, 0, 0, 0, // 8 + 1, 0, 0, 0, // 9 + ], + }, + + indices: { + numComponents: 2, + data: [ + 0, 1, + 0, 2, + 1, 3, + 2, 3, // + 2, 4, + 3, 5, + 4, 5, + 4, 6, + 5, 7, // + 6, 7, + 6, 8, + 7, 9, + 8, 9, + ], + }, +}; +// вызывает gl.createBuffer, gl.bindBuffer, gl.bufferData +const bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); +const skinVAO = twgl.createVAOFromBufferInfo(gl, programInfo, bufferInfo); +``` + +Мы можем определить наши uniform значения, включая матрицу для каждой кости + +``` +// 4 матрицы, одна для каждой кости +const numBones = 4; +const boneArray = new Float32Array(numBones * 16); + +var uniforms = { + projection: m4.orthographic(-20, 20, -10, 10, -1, 1), + view: m4.translation(-6, 0, 0), + bones: boneArray, + color: [1, 0, 0, 1], +}; \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-skybox.md b/webgl/lessons/ru/webgl-skybox.md new file mode 100644 index 000000000..7c6db9dce --- /dev/null +++ b/webgl/lessons/ru/webgl-skybox.md @@ -0,0 +1,454 @@ +Title: WebGL2 Скайбокс +Description: Показываем небо с помощью скайбокса! +TOC: Скайбоксы + +Эта статья является частью серии статей о WebGL. +[Первая статья начинается с основ](webgl-fundamentals.html). +Эта статья продолжается от [статьи о картах окружения](webgl-environment-maps.html). + +*Скайбокс* - это коробка с текстурами на ней, чтобы выглядеть как небо во всех направлениях +или скорее выглядеть как то, что очень далеко, включая горизонт. Представьте, +что вы стоите в комнате, и на каждой стене есть полноразмерный постер какого-то вида, +добавьте постер, чтобы покрыть потолок, показывающий небо, и один для пола, +показывающий землю, и это скайбокс. + +Многие 3D игры делают это, просто создавая куб, делая его действительно большим, помещая +на него текстуру неба. + +Это работает, но имеет проблемы. Одна проблема в том, что у вас есть куб, который нужно +рассматривать в нескольких направлениях, в каком бы направлении ни была обращена камера. Вы хотите, +чтобы все рисовалось далеко, но вы не хотите, чтобы углы куба выходили +за пределы плоскости отсечения. Усложняя эту проблему, по соображениям производительности вы хотите +рисовать близкие вещи перед далекими, потому что GPU, используя [тест буфера глубины](webgl-3d-orthographic.html), +может пропустить рисование пикселей, которые, как он знает, не пройдут +тест. Так что идеально вы должны рисовать скайбокс последним с включенным тестом глубины, но +если вы действительно используете коробку, когда камера смотрит в разных направлениях, +углы коробки будут дальше, чем стороны, вызывая проблемы. + +
+ +Вы можете видеть выше, что нам нужно убедиться, что самая дальняя точка куба находится внутри +усеченной пирамиды, но из-за этого некоторые края куба могут в конечном итоге покрывать +объекты, которые мы не хотим покрывать. + +Типичное решение - отключить тест глубины и рисовать скайбокс первым, но +тогда мы не получаем выгоды от теста буфера глубины, не рисуя пиксели, которые мы +позже покроем вещами в нашей сцене. + +Вместо использования куба давайте просто нарисуем четырехугольник, который покрывает весь холст, и +используем [кубическую карту](webgl-cube-maps.html). Обычно мы используем матрицу проекции вида +для проекции четырехугольника в 3D пространстве. В этом случае мы сделаем наоборот. Мы будем использовать +обратную матрицу проекции вида, чтобы работать в обратном направлении и получить направление, в котором +камера смотрит для каждого пикселя на четырехугольнике. Это даст нам направления для +просмотра в кубическую карту. + +Начиная с [примера карты окружения](webgl-environment-maps.html), я +удалил весь код, связанный с нормалями, поскольку мы не используем их здесь. Затем нам +нужен четырехугольник. + +```js +// Заполняем буфер значениями, которые определяют четырехугольник. +function setGeometry(gl) { + var positions = new Float32Array( + [ + -1, -1, + 1, -1, + -1, 1, + -1, 1, + 1, -1, + 1, 1, + ]); + gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); +} +``` + +Этот четырехугольник заполнит холст, поскольку он уже в пространстве отсечения. Поскольку есть +только 2 значения на вершину, нам нужно изменить код, который устанавливает атрибут. + +```js +// Говорим атрибуту позиции, как получать данные из positionBuffer (ARRAY_BUFFER) +var size = 2; // 2 компонента на итерацию +var type = gl.FLOAT; // данные являются 32-битными числами с плавающей точкой +var normalize = false; // не нормализуем данные +var stride = 0; // 0 = двигаемся вперед на size * sizeof(type) на каждой итерации, чтобы получить следующую позицию +var offset = 0; // начинаем с начала буфера +gl.vertexAttribPointer( + positionLocation, size, type, normalize, stride, offset) +``` + +Далее для вершинного шейдера мы просто устанавливаем `gl_Position` к вершинам четырехугольника напрямую. +Нет необходимости в какой-либо матричной математике, поскольку позиции уже в пространстве отсечения, настроены +для покрытия всего холста. Мы устанавливаем `gl_Position.z` в 1, чтобы гарантировать, что пиксели +имеют самую дальнюю глубину. И мы передаем позицию в фрагментный шейдер. + +```glsl +#version 300 es +in vec4 a_position; +out vec4 v_position; +void main() { + v_position = a_position; + gl_Position = a_position; + gl_Position.z = 1.0; +} +``` + +В фрагментном шейдере мы умножаем позицию на обратную матрицу проекции вида +и делим на w, чтобы перейти из 4D пространства в 3D пространство. + +```glsl +#version 300 es +precision highp float; + +uniform samplerCube u_skybox; +uniform mat4 u_viewDirectionProjectionInverse; + +in vec4 v_position; + +// нам нужно объявить выход для фрагментного шейдера +out vec4 outColor; + +void main() { + vec4 t = u_viewDirectionProjectionInverse * v_position; + outColor = texture(u_skybox, normalize(t.xyz / t.w)); +} +``` + +Наконец нам нужно найти местоположения uniform + +```js +var skyboxLocation = gl.getUniformLocation(program, "u_skybox"); +var viewDirectionProjectionInverseLocation = + gl.getUniformLocation(program, "u_viewDirectionProjectionInverse"); +``` + +и установить их + +```js +// Вычисляем матрицу проекции +var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; +var projectionMatrix = + m4.perspective(fieldOfViewRadians, aspect, 1, 2000); + +// камера движется по кругу на расстоянии 2 единиц от начала координат, смотря на начало координат +var cameraPosition = [Math.cos(time * .1), 0, Math.sin(time * .1)]; +var target = [0, 0, 0]; +var up = [0, 1, 0]; +// Вычисляем матрицу камеры, используя look at. +var cameraMatrix = m4.lookAt(cameraPosition, target, up); + +// Создаем матрицу вида из матрицы камеры. +var viewMatrix = m4.inverse(cameraMatrix); + +// Нас интересует только направление, поэтому убираем трансляцию +viewMatrix[12] = 0; +viewMatrix[13] = 0; +viewMatrix[14] = 0; + +var viewDirectionProjectionMatrix = + m4.multiply(projectionMatrix, viewMatrix); +var viewDirectionProjectionInverseMatrix = + m4.inverse(viewDirectionProjectionMatrix); + +// Устанавливаем uniform +gl.uniformMatrix4fv( + viewDirectionProjectionInverseLocation, false, + viewDirectionProjectionInverseMatrix); + +// Говорим шейдеру использовать текстуру unit 0 для u_skybox +gl.uniform1i(skyboxLocation, 0); +``` + +Обратите внимание выше, что мы вращаем камеру вокруг начала координат, где мы вычисляем +`cameraPosition`. Затем, после преобразования `cameraMatrix` в `viewMatrix`, мы +обнуляем трансляцию, поскольку нас интересует только то, в какую сторону смотрит камера, а не +где она находится. + +Из этого мы умножаем на матрицу проекции, берем обратную, а затем устанавливаем +матрицу. + +{{{example url="../webgl-skybox.html" }}} + +Давайте объединим куб с картой окружения обратно в этот пример. Мы будем использовать +утилиты, упомянутые в [меньше кода больше веселья](webgl-less-code-more-fun.html). + +Нам нужно поместить оба набора шейдеров + +```js +var envmapVertexShaderSource = `... +var envmapFragmentShaderSource = `... +var skyboxVertexShaderSource = `... +var skyboxFragmentShaderSource = `... +``` + +Затем компилируем шейдеры и находим все местоположения атрибутов и uniform + +```js + // Используем twgl для компиляции шейдеров и связывания в программу + const envmapProgramInfo = twgl.createProgramInfo( + gl, [envmapVertexShaderSource, envmapFragmentShaderSource]); + const skyboxProgramInfo = twgl.createProgramInfo( + gl, [skyboxVertexShaderSource, skyboxFragmentShaderSource]); +``` + +Настраиваем наши буферы с данными вершин. twgl уже имеет функции для предоставления этих данных, поэтому мы можем использовать их. + +```js +// создаем буферы и заполняем данными вершин +const cubeBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 1); +const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl); +``` + +и создаем объекты вершинных массивов для каждого + +```js +const cubeVAO = twgl.createVAOFromBufferInfo(gl, envmapProgramInfo, cubeBufferInfo); +const quadVAO = twgl.createVAOFromBufferInfo(gl, skyboxProgramInfo, quadBufferInfo); +``` + +Во время рендеринга мы вычисляем все матрицы + +```js +// камера движется по кругу на расстоянии 2 единиц от начала координат, смотря на начало координат +var cameraPosition = [Math.cos(time * .1) * 2, 0, Math.sin(time * .1) * 2]; +var target = [0, 0, 0]; +var up = [0, 1, 0]; +// Вычисляем матрицу камеры, используя look at. +var cameraMatrix = m4.lookAt(cameraPosition, target, up); + +// Создаем матрицу вида из матрицы камеры. +var viewMatrix = m4.inverse(cameraMatrix); + +// Вращаем куб вокруг оси x +var worldMatrix = m4.xRotation(time * 0.11); + +// Нас интересует только направление, поэтому убираем трансляцию +var viewDirectionMatrix = m4.copy(viewMatrix); +viewDirectionMatrix[12] = 0; +viewDirectionMatrix[13] = 0; +viewDirectionMatrix[14] = 0; + +var viewDirectionProjectionMatrix = m4.multiply( + projectionMatrix, viewDirectionMatrix); +var viewDirectionProjectionInverseMatrix = + m4.inverse(viewDirectionProjectionMatrix); +``` + +Затем сначала рисуем куб + +```js +// рисуем куб +gl.depthFunc(gl.LESS); // используем тест глубины по умолчанию +gl.useProgram(envmapProgramInfo.program); +gl.bindVertexArray(cubeVAO); +twgl.setUniforms(envmapProgramInfo, { + u_world: worldMatrix, + u_view: viewMatrix, + u_projection: projectionMatrix, + u_texture: texture, + u_worldCameraPosition: cameraPosition, +}); +twgl.drawBufferInfo(gl, cubeBufferInfo); +``` + +затем скайбокс + +```js +// рисуем скайбокс + +// позволяем нашему четырехугольнику пройти тест глубины на 1.0 +gl.depthFunc(gl.LEQUAL); + +gl.useProgram(skyboxProgramInfo.program); +gl.bindVertexArray(quadVAO); +twgl.setUniforms(skyboxProgramInfo, { + u_viewDirectionProjectionInverse: viewDirectionProjectionInverseMatrix, + u_skybox: texture, +}); +twgl.drawBufferInfo(gl, quadBufferInfo); +``` + +Обратите внимание, что наш код загрузки текстур также может быть заменен использованием наших вспомогательных +функций + +```js +// Создаем текстуру. +-const texture = gl.createTexture(); +-gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); +- +-const faceInfos = [ +- { +- target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, +- url: 'resources/images/computer-history-museum/pos-x.jpg', +- }, +- { +- target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, +- url: 'resources/images/computer-history-museum/neg-x.jpg', +- }, +- { +- target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, +- url: 'resources/images/computer-history-museum/pos-y.jpg', +- }, +- { +- target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, +- url: 'resources/images/computer-history-museum/neg-y.jpg', +- }, +- { +- target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, +- url: 'resources/images/computer-history-museum/pos-z.jpg', +- }, +- { +- target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, +- url: 'resources/images/computer-history-museum/neg-z.jpg', +- }, +-]; +-faceInfos.forEach((faceInfo) => { +- const {target, url} = faceInfo; +- +- // Загружаем canvas в грань cubemap. +- const level = 0; +- const internalFormat = gl.RGBA; +- const width = 512; +- const height = 512; +- const format = gl.RGBA; +- const type = gl.UNSIGNED_BYTE; +- +- // настраиваем каждую грань так, чтобы она была сразу рендерируемой +- gl.texImage2D(target, level, internalFormat, width, height, 0, format, type, null); +- +- // Асинхронно загружаем изображение +- const image = new Image(); +- image.src = url; +- image.addEventListener('load', function() { +- // Теперь, когда изображение загружено, копируем его в текстуру. +- gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); +- gl.texImage2D(target, level, internalFormat, format, type, image); +- gl.generateMipmap(gl.TEXTURE_CUBE_MAP); +- }); +-}); +-gl.generateMipmap(gl.TEXTURE_CUBE_MAP); +-gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); ++const texture = twgl.createTexture(gl, { ++ target: gl.TEXTURE_CUBE_MAP, ++ src: [ ++ 'resources/images/computer-history-museum/pos-x.jpg', ++ 'resources/images/computer-history-museum/neg-x.jpg', ++ 'resources/images/computer-history-museum/pos-y.jpg', ++ 'resources/images/computer-history-museum/neg-y.jpg', ++ 'resources/images/computer-history-museum/pos-z.jpg', ++ 'resources/images/computer-history-museum/neg-z.jpg', ++ ], ++ min: gl.LINEAR_MIPMAP_LINEAR, ++}); +``` + +и + +{{{example url="../webgl-skybox-plus-environment-map.html" }}} + +Я надеюсь, что эти последние 3 статьи дали вам некоторое представление о том, как использовать кубическую карту. +Обычно, например, берут код [из вычисления освещения](webgl-3d-lighting-spot.html) +и объединяют этот результат с результатами +карты окружения, чтобы создавать материалы, такие как капот автомобиля или полированный пол. +Также есть техника вычисления освещения с использованием кубических карт. Это то же самое, что и +карта окружения, за исключением того, что вместо использования значения, которое вы получаете из карты окружения +как цвета, вы используете его как вход для ваших уравнений освещения. + +и создаем объекты массива вершин для каждого + +```js +const cubeVAO = twgl.createVAOFromBufferInfo(gl, envmapProgramInfo, cubeBufferInfo); +const quadVAO = twgl.createVAOFromBufferInfo(gl, skyboxProgramInfo, quadBufferInfo); +``` + +Во время рендеринга мы вычисляем все матрицы + +```js +// камера движется по кругу на расстоянии 2 единиц от начала координат, смотря на начало координат +var cameraPosition = [Math.cos(time * .1) * 2, 0, Math.sin(time * .1) * 2]; +var target = [0, 0, 0]; +var up = [0, 1, 0]; +// Вычисляем матрицу камеры, используя look at. +var cameraMatrix = m4.lookAt(cameraPosition, target, up); + +// Создаем матрицу вида из матрицы камеры. +var viewMatrix = m4.inverse(cameraMatrix); + +// Вращаем куб вокруг оси x +var worldMatrix = m4.xRotation(time * 0.11); + +// Нас интересует только направление, поэтому убираем перемещение +var viewDirectionMatrix = m4.copy(viewMatrix); +viewDirectionMatrix[12] = 0; +viewDirectionMatrix[13] = 0; +viewDirectionMatrix[14] = 0; + +var viewDirectionProjectionMatrix = m4.multiply( + projectionMatrix, viewDirectionMatrix); +var viewDirectionProjectionInverseMatrix = + m4.inverse(viewDirectionProjectionMatrix); +``` + +Затем сначала рисуем куб + +```js +// рисуем куб +gl.depthFunc(gl.LESS); // используем тест глубины по умолчанию +gl.useProgram(envmapProgramInfo.program); +gl.bindVertexArray(cubeVAO); +twgl.setUniforms(envmapProgramInfo, { + u_world: worldMatrix, + u_view: viewMatrix, + u_projection: projectionMatrix, + u_texture: texture, + u_worldCameraPosition: cameraPosition, +}); +twgl.drawBufferInfo(gl, cubeBufferInfo); +``` + +затем skybox + +```js +// рисуем skybox + +// позволяем нашему четырехугольнику пройти тест глубины на 1.0 +gl.depthFunc(gl.LEQUAL); + +gl.useProgram(skyboxProgramInfo.program); +gl.bindVertexArray(quadVAO); +twgl.setUniforms(skyboxProgramInfo, { + u_viewDirectionProjectionInverse: viewDirectionProjectionInverseMatrix, + u_skybox: texture, +}); +twgl.drawBufferInfo(gl, quadBufferInfo); +``` + +Обратите внимание, что наш код загрузки текстур также может быть заменен использованием наших вспомогательных +функций + +```js +// Создаем текстуру. +const texture = twgl.createTexture(gl, { + target: gl.TEXTURE_CUBE_MAP, + src: [ + 'resources/images/computer-history-museum/pos-x.jpg', + 'resources/images/computer-history-museum/neg-x.jpg', + 'resources/images/computer-history-museum/pos-y.jpg', + 'resources/images/computer-history-museum/neg-y.jpg', + 'resources/images/computer-history-museum/pos-z.jpg', + 'resources/images/computer-history-museum/neg-z.jpg', + ], + min: gl.LINEAR_MIPMAP_LINEAR, +}); +``` + +и + +{{{example url="../webgl-skybox-plus-environment-map.html" }}} + +Я надеюсь, что эти последние 3 статьи дали вам некоторое представление о том, как использовать cubemap. +Это распространено, например, взять код [из вычисления освещения](webgl-3d-lighting-spot.html) +и объединить этот результат с результатами из +карты окружения, чтобы сделать материалы, такие как капот автомобиля или полированный пол. +Есть также техника для вычисления освещения с использованием cubemap. Это то же самое, что и +карта окружения, за исключением того, что вместо использования значения, которое вы получаете из карты окружения +как цвет, вы используете его как вход для ваших уравнений освещения. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-smallest-programs.md b/webgl/lessons/ru/webgl-smallest-programs.md new file mode 100644 index 000000000..d6bae628b --- /dev/null +++ b/webgl/lessons/ru/webgl-smallest-programs.md @@ -0,0 +1,200 @@ +Title: WebGL2 Самые маленькие программы +Description: Самый маленький код для тестирования +TOC: Самые маленькие программы + +Эта статья предполагает, что вы прочитали многие другие статьи, +начиная с [основ](webgl-fundamentals.html). +Если вы их не читали, пожалуйста, начните сначала с них. + +Я не совсем знаю, под что подвести эту статью, потому что у неё есть две +цели. + +1. Показать вам самые маленькие WebGL программы. + + Эти техники супер полезны для тестирования чего-то или + при создании [MCVE для Stack Overflow](https://meta.stackoverflow.com/a/349790/128511) или при попытке сузить + ошибку. + +2. Обучение думать нестандартно + + Я надеюсь написать еще несколько статей на эту тему, + чтобы помочь вам увидеть большую картину, а не только общие паттерны. + [Вот одна](webgl-drawing-without-data.html). + +## Просто очистка + +Вот самая маленькая WebGL программа, которая действительно что-то делает + +```js +const gl = document.querySelector('canvas').getContext('webgl2'); +gl.clearColor(1, 0, 0, 1); // красный +gl.clear(gl.COLOR_BUFFER_BIT); +``` + +Все, что делает эта программа - очищает canvas до красного, но она действительно что-то сделала. + +Подумайте об этом. С помощью только этого вы можете фактически тестировать некоторые вещи. Допустим, +вы [рендерите в текстуру](webgl-render-to-texture.html), но что-то не работает. +Допустим, это точно как в примере в [той статье](webgl-render-to-texture.html). +Вы рендерите 1 или более 3D вещей в текстуру, затем рендерите этот результат на куб. + +Вы ничего не видите. Ну, как простой тест, остановите рендеринг в текстуру с +шейдерами и просто очистите текстуру до известного цвета. + +```js +gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferWithTexture) +gl.clearColor(1, 0, 1, 1); // пурпурный +gl.clear(gl.COLOR_BUFFER_BIT); +``` + +Теперь рендерите с текстурой из framebuffer. Ваш куб становится пурпурным? Если нет, +то ваша проблема не в части рендеринга в текстуру, это что-то другое. + +## Использование `SCISSOR_TEST` и `gl.clear` + +`SCISSOR_TEST` обрезает как рисование, так и очистку до некоторого подпрямоугольника canvas (или текущего framebuffer). + +Вы включаете тест ножниц с помощью + +```js +gl.enable(gl.SCISSOR_TEST); +``` + +и затем вы устанавливаете прямоугольник ножниц в пикселях относительно нижнего левого угла. Он использует те же параметры, +что и `gl.viewport`. + +```js +gl.scissor(x, y, width, height); +``` + +Используя это, можно рисовать прямоугольники с помощью `SCISSOR_TEST` и `gl.clear`. + +Пример + +```js +const gl = document.querySelector('#c').getContext('webgl2'); + +gl.enable(gl.SCISSOR_TEST); + +function drawRect(x, y, width, height, color) { + gl.scissor(x, y, width, height); + gl.clearColor(...color); + gl.clear(gl.COLOR_BUFFER_BIT); +} + +for (let i = 0; i < 100; ++i) { + const x = rand(0, 300); + const y = rand(0, 150); + const width = rand(0, 300 - x); + const height = rand(0, 150 - y); + drawRect(x, y, width, height, [rand(1), rand(1), rand(1), 1]); +} + + +function rand(min, max) { + if (max === undefined) { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; +} +``` + +{{{example url="../webgl-simple-scissor.html"}}} + +Не говорю, что этот конкретный пример очень полезен, но все же +хорошо знать. + +## Использование одной большой `gl.POINTS` + +Как показывают большинство примеров, самая распространенная вещь для выполнения в WebGL +- это создание буферов. Поместить данные вершин в эти буферы. Создать +шейдеры с атрибутами. Настроить атрибуты для извлечения данных из +этих буферов. Затем рисовать, возможно, с uniforms и текстурами, также +используемыми вашими шейдерами. + +Но иногда вы просто хотите протестировать. Допустим, вы хотите просто увидеть, +как что-то рисуется. + +Как насчет этого набора шейдеров + +```glsl +#version 300 es +// вершинный шейдер +void main() { + gl_Position = vec4(0, 0, 0, 1); // центр + gl_PointSize = 120.0; +} +``` + +```glsl +#version 300 es +// фрагментный шейдер +precision highp float; + +out vec4 outColor; + +void main() { + outColor = vec4(1, 0, 0, 1); // красный +} +``` + +И вот код для его использования + +```js +// настройка GLSL программы +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); + +gl.useProgram(program); + +const offset = 0; +const count = 1; +gl.drawArrays(gl.POINTS, offset, count); +``` + +Никаких буферов для создания, никаких uniforms для настройки, и мы получаем одну +точку в центре canvas. + +{{{example url="../webgl-simple-point.html"}}} + +О `gl.POINTS`: Когда вы передаете `gl.POINTS` в `gl.drawArrays`, вы также +обязаны установить `gl_PointSize` в вашем вершинном шейдере в размер в пикселях. Важно +отметить, что разные GPU/Драйверы имеют разный максимальный размер точки, +который вы можете использовать. Вы можете запросить этот максимальный размер с помощью + +``` +const [minSize, maxSize] = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE); +``` + +Спецификация WebGL требует только максимальный размер 1.0. К счастью, +[большинство, если не все GPU и драйверы поддерживают больший размер](https://web3dsurvey.com/webgl/parameters/ALIASED_POINT_SIZE_RANGE). + +После того, как вы установите `gl_PointSize`, когда вершинный шейдер завершится, любое значение, которое вы установили на `gl_Position`, преобразуется +в экранное/canvas пространство в пикселях, затем генерируется квадрат вокруг этой позиции, который составляет +/- gl_PointSize / 2 во всех 4 направлениях. + +Хорошо, я слышу, как вы думаете, ну и что, кто хочет рисовать одну точку. + +Ну, точки автоматически получают бесплатные [координаты текстуры](webgl-3d-textures.html). Они доступны во фрагментном +шейдере с специальной переменной `gl_PointCoord`. Итак, давайте нарисуем текстуру на этой точке. + +Сначала давайте изменим фрагментный шейдер. + +```glsl +#version 300 es +// фрагментный шейдер +precision highp float; + ++uniform sampler tex; + +out vec4 outColor; + +void main() { +- outColor = vec4(1, 0, 0, 1); // красный ++ outColor = texture(tex, gl_PointCoord.xy); +} +``` + +Теперь, чтобы держать это простым, давайте сделаем текстуру с сырыми данными, как мы покрыли в +[статье о текстурах данных](webgl-data-textures.html). + +```js \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-sprites.md b/webgl/lessons/ru/webgl-sprites.md new file mode 100644 index 000000000..ed3f24ab4 --- /dev/null +++ b/webgl/lessons/ru/webgl-sprites.md @@ -0,0 +1,19 @@ +Title: WebGL2 Спрайты +Description: Как делать спрайты в WebGL +TOC: Спрайты + +Эта статья пока что просто заполнитель. + +Спрайты, возможно, уже были покрыты, они просто не назывались "спрайтами". + +Статья о спрайтах - это [статья о том, как рисовать +текст с текстурами](webgl-text-texture.html). Замените +изображения текста изображениями спрайтов, и у вас будут спрайты. + +Связанная статья - это [статья о воспроизведении +функции `drawImage` canvas 2D в WebGL](webgl-2d-drawimage.html) и следующая +статья о [матричных стеках](webgl-2d-matrix-stack.html) + +Вместе эти статьи в основном покрывают спрайты в WebGL. +Если у вас есть другие вещи, которые вы хотите, чтобы были покрыты, рассмотрите +[оставление запроса](https://github.com/gfxfundamentals/webgl-fundamentals/issues/new?assignees=&labels=suggested+topic&template=suggest-topic.md&title=%5BSUGGESTION%5D). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-state-diagram.md b/webgl/lessons/ru/webgl-state-diagram.md new file mode 100644 index 000000000..a904a7145 --- /dev/null +++ b/webgl/lessons/ru/webgl-state-diagram.md @@ -0,0 +1,6 @@ +Title: WebGL2 Диаграмма состояния +Description: WebGL2 Диаграмма состояния +TOC: WebGL2 Диаграмма состояния +Link: webgl/lessons/resources/webgl-state-diagram.html + +[WebGL2 Диаграмма состояния](/webgl/lessons/resources/webgl-state-diagram.html) \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-canvas2d.md b/webgl/lessons/ru/webgl-text-canvas2d.md new file mode 100644 index 000000000..4f7110ea7 --- /dev/null +++ b/webgl/lessons/ru/webgl-text-canvas2d.md @@ -0,0 +1,96 @@ +Title: WebGL2 Текст - Canvas 2D +Description: Как отображать текст, используя 2D canvas, который синхронизирован с WebGL +TOC: Текст - Canvas 2D + + +Эта статья является продолжением [предыдущих статей WebGL о рисовании текста](webgl-text-html.html). +Если вы их не читали, я предлагаю начать там и работать в обратном направлении. + +Вместо использования HTML элементов для текста мы также можем использовать другой canvas, но с +2D контекстом. Без профилирования это просто предположение, что это будет быстрее, +чем использование DOM. Конечно, это также менее гибко. Вы не получаете все +причудливые CSS стили. Но нет HTML элементов для создания и отслеживания. + +Аналогично другим примерам мы создаем контейнер, но на этот раз помещаем +2 canvas в него. + +
+ + +
+ +Затем настраиваем CSS так, чтобы canvas и HTML перекрывались + + .container { + position: relative; + } + + #text { + position: absolute; + left: 0px; + top: 0px; + z-index: 10; + } + +Теперь ищем text canvas во время инициализации и создаем 2D контекст для него. + + // ищем text canvas. + var textCanvas = document.querySelector("#text"); + + // создаем 2D контекст для него + var ctx = textCanvas.getContext("2d"); + +При рисовании, точно так же, как WebGL, нам нужно очищать 2D canvas каждый кадр. + + function drawScene() { + ... + + // Очищаем 2D canvas + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + +И затем мы просто вызываем `fillText` для рисования текста + + ctx.fillText(someMsg, pixelX, pixelY); + +И вот этот пример + +{{{example url="../webgl-text-html-canvas2d.html" }}} + +Почему текст меньше? Потому что это размер по умолчанию для Canvas 2D. +Если вы хотите другие размеры, [проверьте Canvas 2D API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text). + +Другая причина использовать Canvas 2D - это легко рисовать другие вещи. Например, +давайте добавим стрелку + + // рисуем стрелку и текст. + + // сохраняем все настройки canvas + ctx.save(); + + // перемещаем начало координат canvas так, чтобы 0, 0 было в + // верхнем переднем правом углу нашей F + ctx.translate(pixelX, pixelY); + + // рисуем стрелку + ctx.beginPath(); + ctx.moveTo(10, 5); + ctx.lineTo(0, 0); + ctx.lineTo(5, 10); + ctx.moveTo(0, 0); + ctx.lineTo(15, 15); + ctx.stroke(); + + // рисуем текст. + ctx.fillText(someMessage, 20, 20); + + // восстанавливаем canvas к его старым настройкам. + ctx.restore(); + +Здесь мы используем преимущество функции translate Canvas 2D, поэтому нам не нужно делать никаких дополнительных +математических вычислений при рисовании нашей стрелки. Мы просто притворяемся, что рисуем в начале координат, и translate заботится +о перемещении этого начала координат к углу нашей F. + +{{{example url="../webgl-text-html-canvas2d-arrows.html" }}} + +Я думаю, что это покрывает использование Canvas 2D. [Проверьте Canvas 2D API](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) +для большего количества идей. [Далее мы фактически будем рендерить текст в WebGL](webgl-text-texture.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-glyphs.md b/webgl/lessons/ru/webgl-text-glyphs.md new file mode 100644 index 000000000..c0167d9ff --- /dev/null +++ b/webgl/lessons/ru/webgl-text-glyphs.md @@ -0,0 +1,201 @@ +Title: WebGL2 Текст - Использование текстуры глифов +Description: Как отображать текст, используя текстуру, полную глифов +TOC: Текст - Использование текстуры глифов + + +Этот пост является продолжением многих статей о WebGL. Последняя была о +[использовании текстур для рендеринга текста в WebGL](webgl-text-texture.html). +Если вы её не читали, возможно, стоит проверить это перед продолжением. + +В последней статье мы прошли [как использовать текстуру для рисования текста в вашей WebGL +сцене](webgl-text-texture.html). Эта техника очень распространена и отлично подходит +для таких вещей, как в многопользовательских играх, где вы хотите поставить имя над аватаром. +Поскольку это имя редко изменяется, это идеально. + +Допустим, вы хотите рендерить много текста, который часто изменяется, как UI. Учитывая +последний пример в [предыдущей статье](webgl-text-texture.html), очевидное +решение - сделать текстуру для каждой буквы. Давайте изменим последний пример, чтобы сделать +это. + + +var names = [ + + "anna", // 0 + + "colin", // 1 + + "james", // 2 + + "danny", // 3 + + "kalin", // 4 + + "hiro", // 5 + + "eddie", // 6 + + "shu", // 7 + + "brian", // 8 + + "tami", // 9 + + "rick", // 10 + + "gene", // 11 + + "natalie",// 12, + + "evan", // 13, + + "sakura", // 14, + + "kai", // 15, + +]; + + // создаем текстовые текстуры, одну для каждой буквы + var textTextures = [ + + "a", // 0 + + "b", // 1 + + "c", // 2 + + "d", // 3 + + "e", // 4 + + "f", // 5 + + "g", // 6 + + "h", // 7 + + "i", // 8 + + "j", // 9 + + "k", // 10 + + "l", // 11 + + "m", // 12, + + "n", // 13, + + "o", // 14, + + "p", // 14, + + "q", // 14, + + "r", // 14, + + "s", // 14, + + "t", // 14, + + "u", // 14, + + "v", // 14, + + "w", // 14, + + "x", // 14, + + "y", // 14, + + "z", // 14, + ].map(function(name) { + * var textCanvas = makeTextCanvas(name, 10, 26); + +Затем вместо рендеринга одного квада для каждого имени мы будем рендерить один квад для каждой +буквы в каждом имени. + + // настройка для рисования текста. + +// Поскольку каждая буква использует одинаковые атрибуты и одинаковую программу + +// нам нужно сделать это только один раз. + +gl.useProgram(textProgramInfo.program); + +setBuffersAndAttributes(gl, textProgramInfo.attribSetters, textBufferInfo); + + textPositions.forEach(function(pos, ndx) { + + var name = names[ndx]; + + + + // для каждой буквы + + for (var ii = 0; ii < name.length; ++ii) { + + var letter = name.charCodeAt(ii); + + var letterNdx = letter - "a".charCodeAt(0); + + + + // выбираем текстуру буквы + + var tex = textTextures[letterNdx]; + + // используем только позицию 'F' для текста + + // потому что pos в пространстве вида, это означает, что это вектор от глаза к + // некоторой позиции. Итак, перемещаемся вдоль этого вектора обратно к глазу на некоторое расстояние + var fromEye = m4.normalize(pos); + var amountToMoveTowardEye = 150; // потому что F 150 единиц длиной + var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; + var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; + var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; + var desiredTextScale = -1 / gl.canvas.height; // 1x1 пиксели + var scale = viewZ * desiredTextScale; + + var textMatrix = m4.translate(projectionMatrix, viewX, viewY, viewZ); + textMatrix = m4.scale(textMatrix, tex.width * scale, tex.height * scale, 1); + +textMatrix = m4.translate(textMatrix, ii, 0, 0); + + // устанавливаем uniform текстуры + textUniforms.u_texture = tex.texture; + copyMatrix(textMatrix, textUniforms.u_matrix); + setUniforms(textProgramInfo.uniformSetters, textUniforms); + + // Рисуем текст. + gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0); + } + }); + +И вы можете видеть, что это работает + +{{{example url="../webgl-text-glyphs.html" }}} + +К сожалению, это МЕДЛЕННО. Пример ниже не показывает это, но мы индивидуально +рисуем 73 квада. Мы вычисляем 73 матрицы и 219 умножений матриц. Типичный +UI может легко иметь 1000 букв. Это слишком много работы, чтобы получить +разумную частоту кадров. + +Итак, чтобы исправить это, способ, которым это обычно делается, - создать текстуру-атлас, которая содержит все +буквы. Мы прошли, что такое текстура-атлас, когда говорили о [текстурировании 6 +граней куба](webgl-3d-textures.html#texture-atlas). + +Ища в интернете, я нашел [этот простой открытый исходный код текстуры шрифта-атласа](https://opengameart.org/content/8x8-font-chomps-wacky-worlds-beta) + + +Давайте создадим некоторые данные, которые мы можем использовать для помощи в генерации позиций и координат текстуры + +``` +var fontInfo = { + letterHeight: 8, + spaceWidth: 8, + spacing: -1, + textureWidth: 64, + textureHeight: 40, + glyphInfos: { + 'a': { x: 0, y: 0, width: 8, }, + 'b': { x: 8, y: 0, width: 8, }, + 'c': { x: 16, y: 0, width: 8, }, + 'd': { x: 24, y: 0, width: 8, }, + 'e': { x: 32, y: 0, width: 8, }, + 'f': { x: 40, y: 0, width: 8, }, + 'g': { x: 48, y: 0, width: 8, }, + 'h': { x: 56, y: 0, width: 8, }, + 'i': { x: 0, y: 8, width: 8, }, + 'j': { x: 8, y: 8, width: 8, }, + 'k': { x: 16, y: 8, width: 8, }, + 'l': { x: 24, y: 8, width: 8, }, + 'm': { x: 32, y: 8, width: 8, }, + 'n': { x: 40, y: 8, width: 8, }, + 'o': { x: 48, y: 8, width: 8, }, + 'p': { x: 56, y: 8, width: 8, }, + 'q': { x: 0, y: 16, width: 8, }, + 'r': { x: 8, y: 16, width: 8, }, + 's': { x: 16, y: 16, width: 8, }, + 't': { x: 24, y: 16, width: 8, }, + 'u': { x: 32, y: 16, width: 8, }, + 'v': { x: 40, y: 16, width: 8, }, + 'w': { x: 48, y: 16, width: 8, }, + 'x': { x: 56, y: 16, width: 8, }, + 'y': { x: 0, y: 24, width: 8, }, + 'z': { x: 8, y: 24, width: 8, }, + '0': { x: 16, y: 24, width: 8, }, + '1': { x: 24, y: 24, width: 8, }, + '2': { x: 32, y: 24, width: 8, }, + '3': { x: 40, y: 24, width: 8, }, + '4': { x: 48, y: 24, width: 8, }, + '5': { x: 56, y: 24, width: 8, }, + '6': { x: 0, y: 32, width: 8, }, + '7': { x: 8, y: 32, width: 8, }, + '8': { x: 16, y: 32, width: 8, }, + '9': { x: 24, y: 32, width: 8, }, + '-': { x: 32, y: 32, width: 8, }, + '*': { x: 40, y: 32, width: 8, }, + '!': { x: 48, y: 32, width: 8, }, + '?': { x: 56, y: 32, width: 8, }, + }, +}; +``` + +И мы [загрузим изображение точно так же, как мы загружали текстуры раньше](webgl-3d-textures.html) + +``` +// Создаем текстуру. +var glyphTex = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, glyphTex); +// Заполняем текстуру 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/8x8-font.png"; +image.addEventListener('load', function() { + // Теперь, когда изображение загружено, копируем его в текстуру. + gl.bindTexture(gl.TEXTURE_2D, glyphTex); +} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-html.md b/webgl/lessons/ru/webgl-text-html.md new file mode 100644 index 000000000..64e594ee8 --- /dev/null +++ b/webgl/lessons/ru/webgl-text-html.md @@ -0,0 +1,224 @@ +Title: WebGL2 Текст - HTML +Description: Как использовать HTML для отображения текста, который позиционируется в соответствии с WebGL +TOC: Текст - HTML + + +Эта статья является продолжением предыдущих статей WebGL. +Если вы их не читали, я предлагаю [начать там](webgl-3d-perspective.html) +и работать в обратном направлении. + +Частый вопрос: "как рисовать текст в WebGL". Первое, что нужно спросить себя - +какая у вас цель рисования текста. Вы в браузере, браузер отображает текст. +Поэтому ваш первый ответ должен быть - использовать HTML для отображения текста. + +Давайте сначала сделаем самый простой пример: Вы просто хотите нарисовать какой-то текст поверх +вашего WebGL. Мы можем назвать это текстовым наложением. По сути, это текст, который остается +в том же положении. + +Простой способ - создать HTML элемент или элементы и использовать CSS, чтобы они перекрывались. + +Например: Сначала создайте контейнер и поместите в него и canvas, и HTML для наложения. + +
+ +
+
Время:
+
Угол:
+
+
+ +Затем настройте CSS так, чтобы canvas и HTML перекрывались + + .container { + position: relative; + } + #overlay { + position: absolute; + left: 10px; + top: 10px; + } + +Теперь найдите эти элементы во время инициализации и создайте или найдите области, которые хотите +изменить. + + // ищем элементы, которые хотим изменить + var timeElement = document.querySelector("#time"); + var angleElement = document.querySelector("#angle"); + + // Создаем текстовые узлы, чтобы сэкономить время браузера + // и избежать выделений памяти. + var timeNode = document.createTextNode(""); + var angleNode = document.createTextNode(""); + + // Добавляем эти текстовые узлы туда, где они должны быть + timeElement.appendChild(timeNode); + angleElement.appendChild(angleNode); + +Наконец, обновляем узлы при рендеринге + + function drawScene(time) { + var now = time * 0.001; // конвертируем в секунды + + ... + + // конвертируем вращение из радиан в градусы + var angle = radToDeg(rotation[1]); + + // показываем только 0 - 360 + angle = angle % 360; + + // устанавливаем узлы + angleNode.nodeValue = angle.toFixed(0); // без десятичных знаков + timeNode.nodeValue = now.toFixed(2); // 2 десятичных знака + +И вот этот пример + +{{{example url="../webgl-text-html-overlay.html" }}} + +Обратите внимание, как я поместил span внутри div специально для частей, которые хотел изменить. Я предполагаю, +что это быстрее, чем просто использовать div без span и говорить что-то вроде + + timeNode.nodeValue = "Время " + now.toFixed(2); + +Также я использую текстовые узлы, вызывая `node = document.createTextNode()` и позже `node.nodeValue = someMsg`. +Я также мог бы использовать `someElement.innerHTML = someHTML`. Это было бы более гибко, потому что вы могли бы +вставлять произвольные HTML строки, хотя это может быть немного медленнее, поскольку браузер должен создавать +и уничтожать узлы каждый раз, когда вы его устанавливаете. Что лучше - решать вам. + +Важный момент, который нужно усвоить из техники наложения, заключается в том, что WebGL работает в браузере. Помните +использовать функции браузера, когда это уместно. Многие программисты OpenGL привыкли к тому, что им приходится рендерить +каждую часть своего приложения на 100% с нуля, но поскольку WebGL работает в браузере, у него уже есть +множество функций. Используйте их. Это имеет много преимуществ. Например, вы можете использовать CSS стили для +легкого придания этому наложению интересного стиля. + +Например, вот тот же пример, но с добавлением стиля. Фон закруглен, буквы имеют +свечение вокруг них. Есть красная граница. Вы получаете все это практически бесплатно, используя HTML. + +{{{example url="../webgl-text-html-overlay-styled.html" }}} + +Следующая наиболее распространенная вещь, которую хочется сделать - это позиционировать какой-то текст относительно того, что вы рендерите. +Мы можем сделать это и в HTML. + +В этом случае мы снова создадим контейнер с canvas и другим контейнером для нашего движущегося HTML + +
+ +
+
+ +И настроим CSS + + .container { + position: relative; + overflow: none; + } + + #divcontainer { + position: absolute; + left: 0px; + top: 0px; + width: 400px; + height: 300px; + z-index: 10; + overflow: hidden; + + } + + .floating-div { + position: absolute; + } + +Часть `position: absolute;` делает так, чтобы `#divcontainer` позиционировался в абсолютных терминах относительно +первого родителя с другим стилем `position: relative` или `position: absolute`. В данном случае +это контейнер, в котором находятся и canvas, и `#divcontainer`. + +`left: 0px; top: 0px` делает так, чтобы `#divcontainer` выравнивался со всем. `z-index: 10` делает +его плавающим над canvas. И `overflow: hidden` делает так, чтобы его дочерние элементы обрезались. + +Наконец, `.floating-div` будет использоваться для позиционируемого div, который мы создаем. + +Итак, теперь нам нужно найти divcontainer, создать div и добавить его. + + // ищем divcontainer + var divContainerElement = document.querySelector("#divcontainer"); + + // создаем div + var div = document.createElement("div"); + + // назначаем ему CSS класс + div.className = "floating-div"; + + // создаем текстовый узел для его содержимого + var textNode = document.createTextNode(""); + div.appendChild(textNode); + + // добавляем его в divcontainer + divContainerElement.appendChild(div); + + +Теперь мы можем позиционировать div, устанавливая его стиль. + + div.style.left = Math.floor(x) + "px"; + div.style.top = Math.floor(y) + "px"; + textNode.nodeValue = now.toFixed(2); + +Вот пример, где мы просто заставляем div подпрыгивать. + +{{{example url="../webgl-text-html-bouncing-div.html" }}} + +Итак, следующий шаг - мы хотим разместить его относительно чего-то в 3D сцене. +Как мы это делаем? Мы делаем это точно так же, как мы просили GPU сделать это, когда мы +[рассматривали перспективную проекцию](webgl-3d-perspective.html). + +До этого примера мы научились использовать матрицы, как их умножать, +и как применять матрицу проекции для преобразования их в пространство отсечения. Мы передаем все +это в наш шейдер, и он умножает вершины в локальном пространстве и преобразует +их в пространство отсечения. Мы можем сделать всю математику сами в JavaScript. +Затем мы можем умножить пространство отсечения (-1 до +1) в пиксели и использовать +это для позиционирования div. + + gl.drawArrays(...); + + // Мы только что закончили вычислять матрицу для рисования нашей + // F в 3D. + + // выбираем точку в локальном пространстве 'F'. + // X Y Z W + var point = [100, 0, 0, 1]; // это передний верхний правый угол + + // вычисляем позицию в пространстве отсечения + // используя матрицу, которую мы вычислили для F + var clipspace = m4.transformVector(matrix, point); + + // делим X и Y на W точно так же, как это делает GPU. + clipspace[0] /= clipspace[3]; + clipspace[1] /= clipspace[3]; + + // конвертируем из пространства отсечения в пиксели + var pixelX = (clipspace[0] * 0.5 + 0.5) * gl.canvas.width; + var pixelY = (clipspace[1] * -0.5 + 0.5) * gl.canvas.height; + + // позиционируем div + div.style.left = Math.floor(pixelX) + "px"; + div.style.top = Math.floor(pixelY) + "px"; + textNode.nodeValue = now.toFixed(2); + +И вуаля, верхний левый угол нашего div идеально выровнен +с верхним правым передним углом F. + +{{{example url="../webgl-text-html-div.html" }}} + +Конечно, если вы хотите больше текста, создайте больше div. + +{{{example url="../webgl-text-html-divs.html" }}} + +Вы можете посмотреть исходный код последнего примера, чтобы увидеть +детали. Один важный момент - я просто предполагаю, что +создание, добавление и удаление HTML элементов из DOM +медленно, поэтому пример выше создает их и держит их +рядом. Он скрывает неиспользуемые, а не удаляет их +из DOM. Вам нужно будет профилировать, чтобы знать, быстрее ли это. +Это был просто метод, который я выбрал. + +Надеюсь, понятно, как использовать HTML для текста. [Далее мы +рассмотрим использование Canvas 2D для текста](webgl-text-canvas2d.html). \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-text-texture.md b/webgl/lessons/ru/webgl-text-texture.md new file mode 100644 index 000000000..bf1539004 --- /dev/null +++ b/webgl/lessons/ru/webgl-text-texture.md @@ -0,0 +1,502 @@ +Title: WebGL2 Текст - Текстуры +Description: Отображение текста в WebGL с использованием текстур +TOC: Текст - Использование текстуры + + +Эта статья является продолжением многих статей о WebGL. Последняя была о +[использовании Canvas 2D для рендеринга текста поверх WebGL canvas](webgl-text-canvas2d.html). +Если вы ее не читали, возможно, стоит сначала ознакомиться с ней. + +В последней статье мы рассмотрели [как использовать 2D canvas для рисования текста поверх вашей WebGL +сцены](webgl-text-canvas2d.html). Эта техника работает и проста в реализации, но у нее есть +ограничение - текст не может быть скрыт другими 3D объектами. Для этого нам +действительно нужно рисовать текст в WebGL. + +Самый простой способ сделать это - создать текстуры с текстом в них. Вы могли бы, например, +зайти в Photoshop или другую программу для рисования и нарисовать изображение с каким-то текстом. + + + +Затем создать какую-то плоскую геометрию и отобразить ее. Это на самом деле то, как некоторые игры, над которыми я +работал, делали весь свой текст. Например, Locoroco имела только около 270 строк. Она была +локализована на 17 языков. У нас был Excel лист со всеми языками и скрипт, +который запускал Photoshop и генерировал текстуру, одну для каждого сообщения на каждом языке. + +Конечно, вы также можете генерировать текстуры во время выполнения. Поскольку WebGL находится в браузере, +мы снова можем полагаться на Canvas 2D API для помощи в генерации наших текстур. + +Начиная с примеров из [предыдущей статьи](webgl-text-canvas2d.html), +давайте добавим функцию для заполнения 2D canvas каким-то текстом + + var textCtx = document.createElement("canvas").getContext("2d"); + + // Помещает текст в центр canvas. + function makeTextCanvas(text, width, height) { + textCtx.canvas.width = width; + textCtx.canvas.height = height; + textCtx.font = "20px monospace"; + textCtx.textAlign = "center"; + textCtx.textBaseline = "middle"; + textCtx.fillStyle = "black"; + textCtx.clearRect(0, 0, textCtx.canvas.width, textCtx.canvas.height); + textCtx.fillText(text, width / 2, height / 2); + return textCtx.canvas; + } + +Теперь, когда нам нужно рисовать 2 разные вещи в WebGL, 'F' и наш текст, я переключусь на +[использование некоторых вспомогательных функций, как описано в предыдущей статье](webgl-drawing-multiple-things.html). +Если неясно, что такое `programInfo`, `bufferInfo` и т.д., см. ту статью. + +Итак, давайте создадим 'F' и единичный квадрат. + +``` +// Создаем данные для 'F' +var fBufferInfo = primitives.create3DFBufferInfo(gl); +var fVAO = webglUtils.createVAOFromBufferInfo( + gl, fProgramInfo, fBufferInfo); + +// Создаем единичный квадрат для 'текста' +var textBufferInfo = primitives.createXYQuadBufferInfo(gl, 1); +var textVAO = webglUtils.createVAOFromBufferInfo( + gl, textProgramInfo, textBufferInfo); +``` + +XY квадрат - это квадрат размером в 1 единицу. Этот центрирован в начале координат. Будучи размером в 1 единицу, +его границы -0.5, -0.5 и 0.5, 0.5 + +Затем создаем 2 шейдера + + // настраиваем GLSL программы + var fProgramInfo = webglUtils.createProgramInfo( + gl, [fVertexShaderSource, fFragmentShaderSource]); + var textProgramInfo = webglUtils.createProgramInfo( + gl, [textVertexShaderSource, textFragmentShaderSource]); + +И создаем нашу текстовую текстуру. Мы генерируем мипмапы, поскольку текст будет становиться маленьким + + // создаем текстовую текстуру. + var textCanvas = makeTextCanvas("Привет!", 100, 26); + var textWidth = textCanvas.width; + var textHeight = textCanvas.height; + var textTex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, textTex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas); + gl.generateMipmap(gl.TEXTURE_2D); + 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); + +Настраиваем uniforms для 'F' и текста + + var fUniforms = { + u_matrix: m4.identity(), + }; + + var textUniforms = { + u_matrix: m4.identity(), + u_texture: textTex, + }; + +Теперь, когда мы вычисляем матрицы для F, мы начинаем с viewMatrix вместо +viewProjectionMatrix, как в других примерах. Мы умножаем это на части, +которые составляют ориентацию нашей F + + var fViewMatrix = m4.translate(viewMatrix, + translation[0] + xx * spread, translation[1] + yy * spread, translation[2]); + fViewMatrix = m4.xRotate(fViewMatrix, rotation[0]); + fViewMatrix = m4.yRotate(fViewMatrix, rotation[1] + yy * xx * 0.2); + fViewMatrix = m4.zRotate(fViewMatrix, rotation[2] + now + (yy * 3 + xx) * 0.1); + fViewMatrix = m4.scale(fViewMatrix, scale[0], scale[1], scale[2]); + fViewMatrix = m4.translate(fViewMatrix, -50, -75, 0); + +Затем наконец мы умножаем на projectionMatrix при установке нашего uniform значения. + + fUniforms.u_matrix = m4.multiply(projectionMatrix, fViewMatrix); + +Важно отметить здесь, что `projectionMatrix` находится слева. Это позволяет нам +умножать на projectionMatrix, как будто это была первая матрица. Обычно +мы умножаем справа. + +Рисование F выглядит так + + // настраиваем для рисования 'F' + gl.useProgram(fProgramInfo.program); + + // настраиваем атрибуты и буферы для F + gl.bindVertexArray(fVAO); + + fUniforms.u_matrix = m4.multiply(projectionMatrix, fViewMatrix); + + webglUtils.setUniforms(fProgramInfo, fUniforms); + + webglUtils.drawBufferInfo(gl, fBufferInfo); + +Для текста мы начинаем с projectionMatrix и затем получаем только позицию +из fViewMatrix, которую мы сохранили ранее. Это даст нам пространство перед видом. +Нам также нужно масштабировать наш единичный квадрат, чтобы соответствовать размерам текстуры. + + // используем только позицию вида 'F' для текста + var textMatrix = m4.translate(projectionMatrix, + fViewMatrix[12], fViewMatrix[13], fViewMatrix[14]); + // масштабируем F до нужного нам размера. + textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1); + +И затем рендерим текст + + // настраиваем для рисования текста. + gl.useProgram(textProgramInfo.program); + + gl.bindVertexArray(textVAO); + + m4.copy(textMatrix, textUniforms.u_matrix); + webglUtils.setUniforms(textProgramInfo, textUniforms); + + // Рисуем текст. + webglUtils.drawBufferInfo(gl, textBufferInfo); + +Итак, вот это + +{{{example url="../webgl-text-texture.html" }}} + +Вы заметите, что иногда части нашего текста покрывают части наших F. Это потому, что +мы рисуем квадрат. Цвет по умолчанию canvas - прозрачный черный (0,0,0,0), и +мы рисуем этот цвет в квадрате. Мы могли бы вместо этого смешивать наши пиксели. + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +Это заставляет брать исходный пиксель (цвет из нашего фрагментного шейдера) и комбинировать его +с целевым пикселем (цветом в canvas) согласно функции смешивания. Мы установили +функцию смешивания на `SRC_ALPHA` для источника и `ONE_MINUS_SRC_ALPHA` для цели. + + result = dest * (1 - src_alpha) + src * src_alpha + +так, например, если цель зеленая `0,1,0,1`, а источник красный `1,0,0,1`, у нас будет + + src = [1, 0, 0, 1] + dst = [0, 1, 0, 1] + src_alpha = src[3] // это 1 + result = dst * (1 - src_alpha) + src * src_alpha + + // что то же самое, что + result = dst * 0 + src * 1 + + // что то же самое, что + result = src + +Для частей текстуры с прозрачным черным `0,0,0,0` + + src = [0, 0, 0, 0] + dst = [0, 1, 0, 1] + src_alpha = src[3] // это 0 + result = dst * (1 - src_alpha) + src * src_alpha + + // что то же самое, что + result = dst * 1 + src * 0 + + // что то же самое, что + result = dst + +Вот результат с включенным смешиванием. + +{{{example url="../webgl-text-texture-enable-blend.html" }}} + +Вы можете видеть, что это лучше, но все еще не идеально. Если вы посмотрите +близко, иногда увидите эту проблему + + + +Что происходит? Мы сейчас рисуем F, затем его текст, затем следующий F, +затем его текст повторяем. У нас все еще есть [буфер глубины](webgl-3d-orthographic.html), поэтому когда мы рисуем +текст для F, даже хотя смешивание заставило некоторые пиксели остаться цветом фона, +буфер глубины все еще был обновлен. Когда мы рисуем следующий F, если части этого F находятся +за этими пикселями от какого-то ранее нарисованного текста, они не будут нарисованы. + +Мы только что столкнулись с одной из самых сложных проблем рендеринга 3D на GPU. +**Прозрачность имеет проблемы**. + +Самое распространенное решение для практически всего прозрачного +рендеринга - это рисовать все непрозрачные вещи сначала, затем после этого рисовать все прозрачные +вещи, отсортированные по z расстоянию с тестированием буфера глубины включенным, но обновлением буфера глубины выключенным. + +Давайте сначала отделим рисование непрозрачных вещей (F) от прозрачных вещей (текст). +Сначала мы объявим что-то для запоминания позиций текста. + + var textPositions = []; + +И в цикле для рендеринга F мы запомним эти позиции + + // запоминаем позицию для текста + textPositions.push([fViewMatrix[12], fViewMatrix[13], fViewMatrix[14]]); + +Перед тем как рисовать 'F', мы отключим смешивание и включим запись в буфер глубины + + gl.disable(gl.BLEND); + gl.depthMask(true); + +Для рисования текста мы включим смешивание и отключим запись в буфер глубины. + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.depthMask(false); + +И затем рисуем текст во всех позициях, которые мы сохранили + + textPositions.forEach(function(pos) { + // используем только позицию вида 'F' для текста + var textMatrix = m4.translate(projectionMatrix, + pos[0], pos[1], pos[2]); + // масштабируем F до нужного нам размера. + textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1); + + // настраиваем для рисования текста. + gl.useProgram(textProgramInfo.program); + + gl.bindVertexArray(textVAO); + + m4.copy(textMatrix, textUniforms.u_matrix); + webglUtils.setUniforms(textProgramInfo, textUniforms); + + // Рисуем текст. + webglUtils.drawBufferInfo(gl, textBufferInfo); + }); + +И теперь это в основном работает + +{{{example url="../webgl-text-texture-separate-opaque-from-transparent.html" }}} + +Обратите внимание, мы не сортировали, как я упомянул выше. В данном случае, поскольку мы рисуем в основном непрозрачный текст, +вероятно, не будет заметной разницы, если мы отсортируем, поэтому я оставлю это для какой-то +другой статьи. + +Другая проблема в том, что текст пересекается со своей собственной 'F'. Для этого действительно +нет конкретного решения. Если бы вы делали MMO и хотели, чтобы текст каждого +игрока всегда появлялся, вы могли бы попытаться заставить текст появляться над головой. Просто переместите +его +Y на какое-то количество единиц, достаточно, чтобы убедиться, что он всегда был выше игрока. + +Вы также можете переместить его вперед к камере. Давайте сделаем это здесь просто так. +Поскольку 'pos' находится в пространстве вида, это означает, что он относительно глаза (который находится в 0,0,0 в пространстве вида). +Поэтому если мы нормализуем его, мы получим единичный вектор, указывающий от глаза к этой точке, который мы можем затем +умножить на какое-то количество, чтобы переместить текст на определенное количество единиц к глазу или от него. + + // потому что pos находится в пространстве вида, это означает, что это вектор от глаза к + // какой-то позиции. Поэтому перемещаем вдоль этого вектора назад к глазу на какое-то расстояние + var fromEye = m4.normalize(pos); + var amountToMoveTowardEye = 150; // потому что F длиной 150 единиц + var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; + var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; + var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; + + var textMatrix = m4.translate(projectionMatrix, + viewX, viewY, viewZ); + // масштабируем F до нужного нам размера. + textMatrix = m4.scale(textMatrix, textWidth, textHeight, 1); + +Вот это. + +{{{example url="../webgl-text-texture-moved-toward-view.html" }}} + +Вы все еще можете заметить проблему с краями букв. + + + +Проблема здесь в том, что Canvas 2D API производит только предварительно умноженные альфа значения. +Когда мы загружаем содержимое canvas в текстуру, WebGL пытается отменить предварительное умножение +значений, но он не может сделать это идеально, потому что предварительно умноженная альфа теряет информацию. + +Чтобы исправить это, давайте скажем WebGL не отменять предварительное умножение + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + +Это говорит WebGL поставлять предварительно умноженные альфа значения в `gl.texImage2D` и `gl.texSubImage2D`. +Если данные, переданные в `gl.texImage2D`, уже предварительно умножены, как это есть для данных Canvas 2D, то +WebGL может просто пропустить их. + +Нам также нужно изменить функцию смешивания + + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + +Старая умножала исходный цвет на его альфа. Это то, что означает `SRC_ALPHA`. Но +теперь данные нашей текстуры уже были умножены на его альфа. Это то, что означает предварительно умноженная. +Поэтому нам не нужно, чтобы GPU делал умножение. Установка на `ONE` означает умножить на 1. + +{{{example url="../webgl-text-texture-premultiplied-alpha.html" }}} + +Края исчезли сейчас. + +Что, если вы хотите сохранить текст фиксированного размера, но все еще правильно сортировать? Ну, если вы помните +из [статьи о перспективе](webgl-3d-perspective.html), наша матрица перспективы собирается +масштабировать наш объект на `-Z`, чтобы заставить его становиться меньше на расстоянии. Поэтому мы можем просто масштабировать +на `-Z` умножить на какой-то желаемый-масштаб, чтобы компенсировать. + + ... + // потому что pos находится в пространстве вида, это означает, что это вектор от глаза к + // какой-то позиции. Поэтому перемещаем вдоль этого вектора назад к глазу на какое-то расстояние + var fromEye = normalize(pos); + var amountToMoveTowardEye = 150; // потому что F длиной 150 единиц + var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye; + var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye; + var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye; + var desiredTextScale = -1 / gl.canvas.height; // 1x1 пиксели + var scale = viewZ * desiredTextScale; + + var textMatrix = m4.translate(projectionMatrix, + viewX, viewY, viewZ); + // масштабируем F до нужного нам размера. + textMatrix = m4.scale(textMatrix, textWidth * scale, textHeight * scale, 1); + ... + +{{{example url="../webgl-text-texture-consistent-scale.html" }}} + +Если вы хотите рисовать разный текст у каждого F, вы должны создать новую текстуру для каждого +F и просто обновить текстовые uniforms для этого F. + + // создаем текстовые текстуры, одну для каждого F + var textTextures = [ + "анна", // 0 + "коллин", // 1 + "джеймс", // 2 + "дэнни", // 3 + "калин", // 4 + "хиро", // 5 + "эдди", // 6 + "шу", // 7 + "брайан", // 8 + "тами", // 9 + "рик", // 10 + "джин", // 11 + "натали",// 12, + "эван", // 13, + "сакура", // 14, + "кай", // 15, + ].map(function(name) { + var textCanvas = makeTextCanvas(name, 100, 26); + var textWidth = textCanvas.width; + var textHeight = textCanvas.height; + var textTex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, textTex); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas); + gl.generateMipmap(gl.TEXTURE_2D); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + return { + texture: textTex, + width: textWidth, + height: textHeight, + }; + }); + +Затем во время рендеринга выбираем текстуру + + textPositions.forEach(function(pos, ndx) { + + // выбираем текстуру + var tex = textTextures[ndx]; + +Используем размер этой текстуры в наших матричных вычислениях + + var textMatrix = m4.translate(projectionMatrix, + pos[0], pos[1], pos[2]); + // масштабируем F до нужного нам размера. + textMatrix = m4.scale(textMatrix, tex.width * scale, tex.height * scale, 1); + +и устанавливаем uniform для текстуры перед рисованием + + textUniforms.u_texture = tex.texture; + +{{{example url="../webgl-text-texture-different-text.html" }}} + +Мы использовали черный для рисования текста в canvas. +Было бы более полезно, если бы мы рендерили текст белым. Тогда мы могли бы умножить +текст на цвет и сделать его любым цветом, который мы хотим. + +Сначала мы изменим текстовый шейдер, чтобы умножать на цвет + + ... + in vec2 v_texcoord; + + uniform sampler2D u_texture; + uniform vec4 u_color; + + out vec4 outColor; + + void main() { + outColor = texture2D(u_texture, v_texcoord) * u_color; + } + + +И когда мы рисуем текст в canvas, используем белый + + textCtx.fillStyle = "white"; + +Затем мы сделаем некоторые цвета + + // цвета, 1 для каждого F + var colors = [ + [0.0, 0.0, 0.0, 1], // 0 + [1.0, 0.0, 0.0, 1], // 1 + [0.0, 1.0, 0.0, 1], // 2 + [1.0, 1.0, 0.0, 1], // 3 + [0.0, 0.0, 1.0, 1], // 4 + [1.0, 0.0, 1.0, 1], // 5 + [0.0, 1.0, 1.0, 1], // 6 + [0.5, 0.5, 0.5, 1], // 7 + [0.5, 0.0, 0.0, 1], // 8 + [0.0, 0.0, 0.0, 1], // 9 + [0.5, 5.0, 0.0, 1], // 10 + [0.0, 5.0, 0.0, 1], // 11 + [0.5, 0.0, 5.0, 1], // 12, + [0.0, 0.0, 5.0, 1], // 13, + [0.5, 5.0, 5.0, 1], // 14, + [0.0, 5.0, 5.0, 1], // 15, + ]; + +Во время рисования мы выбираем цвет + + // устанавливаем цвет uniform + textUniforms.u_color = colors[ndx]; + +Цвета + +{{{example url="../webgl-text-texture-different-colors.html" }}} + +Эта техника на самом деле является техникой, которую большинство браузеров используют, когда они ускорены GPU. +Они генерируют текстуры с вашим HTML содержимым и всеми различными стилями, которые вы применили, +и пока это содержимое не изменяется, они могут просто рендерить текстуру +снова, когда вы прокручиваете и т.д. Конечно, если вы обновляете вещи все время, то +эта техника может стать немного медленной, потому что перегенерация текстур и их повторная загрузка +в GPU - это относительно медленная операция. + +В [следующей статье мы рассмотрим технику, которая, вероятно, лучше для случаев, когда +вещи обновляются часто](webgl-text-glyphs.html). + +
+

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

+

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

+

+Ну, честно говоря, не очень распространено масштабировать 2D текст в 3D. Посмотрите на большинство игр +или 3D редакторов, и вы увидите, что текст почти всегда одного постоянного размера +независимо от того, насколько далеко или близко к камере он находится. На самом деле часто этот текст +может быть нарисован в 2D вместо 3D, так что даже если кто-то или что-то находится +за чем-то другим, как товарищ по команде за стеной, вы все еще можете читать текст. +

+

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

+
    +
  • Создайте разные размеры текстур с шрифтами при разных разрешениях. Вы затем +используете текстуры более высокого разрешения, когда текст становится больше. Это называется +LODing (использование разных Уровней Детализации).
  • +
  • Другой был бы рендеринг текстур с точным правильным размером +текста каждый кадр. Это, вероятно, было бы действительно медленно.
  • +
  • Еще один был бы сделать 2D текст из геометрии. Другими словами, вместо +рисования текста в текстуру, сделать текст из множества и множества треугольников. Это +работает, но у этого есть другие проблемы в том, что маленький текст не будет рендериться хорошо, а большой +текст вы начнете видеть треугольники.
  • +
  • Еще один - это использовать очень специальные шейдеры, которые рендерят кривые. Это очень круто, +но далеко за пределами того, что я могу объяснить здесь.
  • +
+
\ No newline at end of file diff --git a/webgl/lessons/ru/webgl-texture-units.md b/webgl/lessons/ru/webgl-texture-units.md new file mode 100644 index 000000000..829ba11a2 --- /dev/null +++ b/webgl/lessons/ru/webgl-texture-units.md @@ -0,0 +1,119 @@ +Title: Текстурные юниты в WebGL2 +Description: Что такое текстурные юниты в WebGL? +TOC: Текстурные юниты + +Эта статья поможет вам представить, как устроены текстурные юниты в WebGL. Есть [похожая статья про атрибуты](webgl-attributes.html). + +В качестве подготовки рекомендуется прочитать [Как работает WebGL](webgl-how-it-works.html), +[WebGL: шейдеры и GLSL](webgl-shaders-and-glsl.html), +а также [WebGL: текстуры](webgl-3d-textures.html). + +## Текстурные юниты + +В WebGL есть текстуры. Текстуры — это двумерные массивы данных, которые можно передавать в шейдер. В шейдере объявляется *uniform sampler* примерно так: + +```glsl +uniform sampler2D someTexture; +``` + +Но как шейдер узнаёт, какую текстуру использовать для `someTexture`? + +Здесь и появляются текстурные юниты. Текстурные юниты — это **глобальный массив** ссылок на текстуры. Можно представить, что если бы WebGL был написан на JavaScript, глобальное состояние выглядело бы так: + +```js +const gl = { + activeTextureUnit: 0, + textureUnits: [ + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + ]; +} +``` + +Как видно, `textureUnits` — это массив. Вы присваиваете текстуру одной из *точек привязки* (bind points) в этом массиве текстурных юнитов. Например, назначим `ourTexture` в текстурный юнит 5: + +```js +// при инициализации +const ourTexture = gl.createTexture(); +// здесь код инициализации текстуры + +... + +// при рендере +const indexOfTextureUnit = 5; +gl.activeTexture(gl.TEXTURE0 + indexOfTextureUnit); +gl.bindTexture(gl.TEXTURE_2D, ourTexture); +``` + +Затем вы сообщаете шейдеру, к какому юниту привязана текстура, вызвав: + +```js +gl.uniform1i(someTextureUniformLocation, indexOfTextureUnit); +``` + +Если бы функции `activeTexture` и `bindTexture` WebGL были реализованы на JavaScript, они выглядели бы примерно так: + +```js +// ПСЕВДОКОД!!! +gl.activeTexture = function(unit) { + gl.activeTextureUnit = unit - gl.TEXTURE0; // переводим в индекс с нуля +}; + +gl.bindTexture = function(target, texture) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + textureUnit[target] = texture; +}; +``` + +Можно представить, как работают и другие функции для текстур. Все они принимают `target`, например `gl.texImage2D(target, ...)` или `gl.texParameteri(target)`. Их реализация могла бы быть такой: + +```js +// ПСЕВДОКОД!!! +gl.texImage2D = function(target, level, internalFormat, width, height, border, format, type, data) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + const texture = textureUnit[target]; + texture.mips[level] = convertDataToInternalFormat(internalFormat, width, height, format, type, data); +} + +gl.texParameteri = function(target, pname, value) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + const texture = textureUnit[target]; + texture[pname] = value; +} +``` + +Из приведённого выше псевдокода видно, что `gl.activeTexture` устанавливает внутреннюю глобальную переменную WebGL — индекс массива текстурных юнитов. После этого все остальные функции для текстур используют `target` (первый аргумент), который указывает на bind point текущего текстурного юнита. + +## Максимальное количество текстурных юнитов + +WebGL требует, чтобы реализация поддерживала минимум 32 текстурных юнита. Узнать, сколько поддерживается, можно так: + +```js +const maxTextureUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); +``` + +Обратите внимание, что для вершинных и фрагментных шейдеров могут быть разные лимиты на количество юнитов. Узнать их можно так: + +```js +const maxVertexShaderTextureUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); +const maxFragmentShaderTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); +``` + +Каждый из них должен поддерживать минимум 16 текстурных юнитов. + +Допустим: + +```js +maxTextureUnits = 32 +maxVertexShaderTextureUnits = 16 +maxFragmentShaderTextureUnits = 32 +``` + +Это значит, что если вы используете, например, 2 текстурных юнита в вершинном шейдере, то для фрагментного останется только 30, так как общий максимум — 32. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-tips.md b/webgl/lessons/ru/webgl-tips.md new file mode 100644 index 000000000..2af385642 --- /dev/null +++ b/webgl/lessons/ru/webgl-tips.md @@ -0,0 +1,328 @@ +Title: Советы по WebGL2 +Description: Мелкие нюансы, которые могут вызвать затруднения в WebGL +TOC: # + +Эта статья — сборник мелких проблем, с которыми вы можете столкнуться при работе с WebGL, но которые слишком малы для отдельной статьи. + +--- + + + +# Как сделать скриншот канваса + +В браузере есть по сути две функции для создания скриншота: +Старая — [`canvas.toDataURL`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) +и новая, более удобная — [`canvas.toBlob`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) + +Кажется, что сделать скриншот просто — достаточно добавить такой код: + +```html + ++ +``` + +```js +const elem = document.querySelector('#screenshot'); +elem.addEventListener('click', () => { + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); +}); + +const saveBlob = (function() { + const a = document.createElement('a'); + document.body.appendChild(a); + a.style.display = 'none'; + return function saveData(blob, fileName) { + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName; + a.click(); + }; +}()); +``` + +Вот пример из [статьи про анимацию](webgl-animation.html) с этим кодом и немного CSS для кнопки: + +{{{example url="../webgl-tips-screenshot-bad.html"}}} + +Когда я попробовал — получил вот такой скриншот: + +
+ +Да, это просто пустое изображение. + +Возможно, у вас сработает (зависит от браузера/ОС), но обычно не работает. + +Проблема в том, что для производительности и совместимости браузер по умолчанию очищает буфер рисования WebGL-канваса после отрисовки. + +Есть три решения: + +1. вызвать функцию рендера прямо перед захватом + + Код, который мы использовали, был функцией `drawScene`. Лучше сделать так, чтобы эта функция не меняла состояние, и тогда можно вызывать её для захвата. + + ```js + elem.addEventListener('click', () => { + + drawScene(); + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); + }); + ``` + +2. вызвать код захвата внутри рендер-цикла + + В этом случае мы просто ставим флаг, что хотим сделать захват, а в рендер-цикле реально делаем захват: + + ```js + let needCapture = false; + elem.addEventListener('click', () => { + needCapture = true; + }); + ``` + + а в рендер-цикле (например, в `drawScene`), после отрисовки: + + ```js + function drawScene(time) { + ... + + + if (needCapture) { + + needCapture = false; + + canvas.toBlob((blob) => { + + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + + }); + + } + + ... + } + ``` + +3. Установить `preserveDrawingBuffer: true` при создании WebGL-контекста + + ```js + const gl = someCanvas.getContext('webgl2', {preserveDrawingBuffer: true}); + ``` + + Это заставит WebGL не очищать канвас после композитинга с остальной страницей, но может помешать некоторым оптимизациям. + +Я бы выбрал вариант №1. Для этого лучше разделить код, который обновляет состояние, и код, который рисует. + +```js + var then = 0; + +- requestAnimationFrame(drawScene); ++ requestAnimationFrame(renderLoop); + ++ function renderLoop(now) { ++ // Переводим в секунды ++ now *= 0.001; ++ // Разница со временем предыдущего кадра ++ var deltaTime = now - then; ++ // Запоминаем время ++ then = now; ++ ++ // Каждый кадр увеличиваем вращение ++ rotation[1] += rotationSpeed * deltaTime; ++ ++ drawScene(); ++ ++ // Следующий кадр ++ requestAnimationFrame(renderLoop); ++ } + + // Рисуем сцену ++ function drawScene() { +- function drawScene(now) { +- // Переводим в секунды +- now *= 0.001; +- // Разница со временем предыдущего кадра +- var deltaTime = now - then; +- // Запоминаем время +- then = now; +- +- // Каждый кадр увеличиваем вращение +- rotation[1] += rotationSpeed * deltaTime; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + ... + +- // Следующий кадр +- requestAnimationFrame(drawScene); + } +``` + +Теперь можно просто вызвать `drawScene` перед захватом: + +```js +elem.addEventListener('click', () => { ++ drawScene(); + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); +}); +``` + +Теперь всё должно работать. + +{{{example url="../webgl-tips-screenshot-good.html" }}} + +Если посмотреть на полученное изображение, фон будет прозрачным. Подробнее — [в этой статье](webgl-and-alpha.html). + +--- + + + +# Как не очищать канвас + +Допустим, вы хотите дать пользователю рисовать анимированным объектом. Нужно передать `preserveDrawingBuffer: true` при создании WebGL-контекста. Это не даст браузеру очищать канвас. + +Возьмём последний пример из [статьи про анимацию](webgl-animation.html): + +```js +var canvas = document.querySelector("#canvas"); +-var gl = canvas.getContext("webgl2"); ++var gl = canvas.getContext("webgl2", {preserveDrawingBuffer: true}); +``` + +и изменим вызов `gl.clear`, чтобы очищался только depth-буфер: + +``` +-// Очищаем канвас. +-gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); ++// Очищаем только depth-буфер. ++gl.clear(gl.DEPTH_BUFFER_BIT); +``` + +{{{example url="../webgl-tips-preservedrawingbuffer.html" }}} + +Обратите внимание: если делать полноценную программу для рисования, это не решение, так как браузер всё равно очистит канвас при изменении его размера. Мы меняем размер в зависимости от размера отображения, а он меняется при изменении окна, загрузке файла (например, в другой вкладке), появлении статус-бара, повороте телефона и т.д. + +Если делать настоящее приложение для рисования — [рендерьте в текстуру](webgl-render-to-texture.html). + +--- + + + +# Получение ввода с клавиатуры + +Если вы делаете полноэкранное WebGL-приложение, то всё просто. Но часто хочется, чтобы канвас был частью страницы, и чтобы при клике по нему он принимал ввод с клавиатуры. По умолчанию канвас не получает ввод с клавиатуры. Чтобы это исправить, задайте ему [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex) 0 или больше. Например: + +```html + +``` + +Появляется новая проблема: любой элемент с `tabindex` получает обводку при фокусе. Чтобы убрать её, добавьте CSS: + +```css +canvas:focus { + outline:none; +} +``` + +Для примера — три канваса: + +```html + + + +``` + +и CSS только для последнего: + +```css +#c3:focus { + outline: none; +} +``` + +Навесим одинаковые обработчики на все: + +```js +document.querySelectorAll('canvas').forEach((canvas) => { + const ctx = canvas.getContext('2d'); + + function draw(str) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(str, canvas.width / 2, canvas.height / 2); + } + draw(canvas.id); + + canvas.addEventListener('focus', () => { + draw('есть фокус, нажмите клавишу'); + }); + + canvas.addEventListener('blur', () => { + draw('фокус потерян'); + }); + + canvas.addEventListener('keydown', (e) => { + draw(`keyCode: ${e.keyCode}`); + }); +}); +``` + +Обратите внимание: первый канвас не принимает ввод с клавиатуры. +Второй — принимает, но с обводкой. Третий — и принимает, и без обводки. + +{{{example url="../webgl-tips-tabindex.html"}}} + +--- + + + +# WebGL-анимация как фон страницы + +Частый вопрос — как сделать WebGL-анимацию фоном страницы? + +Есть два очевидных способа: + +* Задать канвасу CSS `position: fixed`, например: + +```css +#canvas { + position: fixed; + left: 0; + top: 0; + z-index: -1; + ... +} +``` + +и `z-index: -1`. + +Минус: ваш JS должен интегрироваться со страницей, и если страница сложная, нужно следить, чтобы код WebGL не конфликтовал с остальным JS. + +* Использовать `iframe` + +Так сделано [на главной странице этого сайта](/). + +Вставьте в страницу iframe, например: + +```html + +
+ Ваш контент. +
+``` + +Затем стилизуйте iframe, чтобы он занимал всё окно и был на заднем плане (почти как выше для канваса), плюс уберите рамку: + +```css +#background { + position: fixed; + width: 100vw; + height: 100vh; + left: 0; + top: 0; + z-index: -1; + border: none; + pointer-events: none; +} +``` + +{{{example url="../webgl-tips-html-background.html"}}} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl-visualizing-the-camera.md b/webgl/lessons/ru/webgl-visualizing-the-camera.md new file mode 100644 index 000000000..684b0d129 --- /dev/null +++ b/webgl/lessons/ru/webgl-visualizing-the-camera.md @@ -0,0 +1,464 @@ +Title: Визуализация камеры в WebGL2 +Description: Как нарисовать frustum камеры +TOC: Визуализация камеры + +Эта статья предполагает, что вы прочитали [статью про множественные виды](webgl-multiple-views.html). +Если вы её не читали, пожалуйста, [прочитайте сначала](webgl-multiple-views.html). + +Также предполагается, что вы прочитали статью [меньше кода — больше веселья](webgl-less-code-more-fun.html), +так как здесь используется библиотека оттуда для упрощения примера. Если вы не понимаете, +что такое буферы, vertex arrays и атрибуты, или что означает функция `twgl.setUniforms` +для установки uniform-переменных и т.д., то стоит вернуться назад и +[прочитать основы](webgl-fundamentals.html). + +Часто полезно визуализировать то, что видит камера — её "frustum". Это удивительно просто. +Как указано в статьях про [ортографическую](webgl-3d-orthographic.html) и [перспективную](webgl-3d-perspective.html) проекции, +эти матрицы проекции преобразуют некоторое пространство в коробку от -1 до +1 в clip space. +Кроме того, матрица камеры — это просто матрица, представляющая положение и ориентацию камеры в мировом пространстве. + +Итак, первое, что должно быть очевидно: если мы просто используем матрицу камеры для рисования чего-то, +у нас будет объект, представляющий камеру. Сложность в том, что камера не может видеть себя, +но используя техники из [статьи про множественные виды](webgl-multiple-views.html), мы можем иметь 2 вида. +Мы будем использовать разные камеры в каждом виде. Второй вид будет смотреть на первый и сможет видеть +объект, который мы рисуем для представления камеры, используемой в другом виде. + +Сначала создадим данные для представления камеры. Сделаем куб и добавим конус на конец. +Будем рисовать это линиями. Используем [индексы](webgl-indexed-vertices.html) для соединения вершин. + +[Камеры](webgl-3d-camera.html) смотрят в направлении -Z, поэтому поместим куб и конус на положительную сторону +с конусом, открытым в сторону -Z. + +Сначала линии куба: + +```js +// создаём геометрию для камеры +function createCameraBufferInfo(gl) { + // сначала добавим куб. Он идёт от 1 до 3, + // потому что камеры смотрят вниз по -Z, поэтому мы хотим, + // чтобы камера начиналась с Z = 0. + const positions = [ + -1, -1, 1, // вершины куба + 1, -1, 1, + -1, 1, 1, + 1, 1, 1, + -1, -1, 3, + 1, -1, 3, + -1, 1, 3, + 1, 1, 3, + ]; + const indices = [ + 0, 1, 1, 3, 3, 2, 2, 0, // индексы куба + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 3, 7, 2, 6, + ]; + return twgl.createBufferInfoFromArrays(gl, { + position: positions, + indices, + }); +} +``` + +Затем добавим линии конуса: + +```js +// создаём геометрию для камеры +function createCameraBufferInfo(gl) { + // сначала добавим куб. Он идёт от 1 до 3, + // потому что камеры смотрят вниз по -Z, поэтому мы хотим, + // чтобы камера начиналась с Z = 0. + // Поместим конус перед этим кубом, открытый + // в сторону -Z + const positions = [ + -1, -1, 1, // вершины куба + 1, -1, 1, + -1, 1, 1, + 1, 1, 1, + -1, -1, 3, + 1, -1, 3, + -1, 1, 3, + 1, 1, 3, + 0, 0, 1, // вершина конуса + ]; + const indices = [ + 0, 1, 1, 3, 3, 2, 2, 0, // индексы куба + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 3, 7, 2, 6, + ]; + // добавляем сегменты конуса + const numSegments = 6; + const coneBaseIndex = positions.length / 3; + const coneTipIndex = coneBaseIndex - 1; + for (let i = 0; i < numSegments; ++i) { + const u = i / numSegments; + const angle = u * Math.PI * 2; + const x = Math.cos(angle); + const y = Math.sin(angle); + positions.push(x, y, 0); + // линия от вершины к краю + indices.push(coneTipIndex, coneBaseIndex + i); + // линия от точки на краю к следующей точке на краю + indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments); + } + return twgl.createBufferInfoFromArrays(gl, { + position: positions, + indices, + }); +} +``` + +И наконец добавим масштаб, потому что наша F высотой 150 единиц, а эта камера размером 2-3 единицы, +она будет крошечной рядом с нашей F. Мы можем масштабировать её, умножая на матрицу масштаба при рисовании, +или можем масштабировать сами данные здесь. + +```js +function createCameraBufferInfo(gl, scale = 1) { + // сначала добавим куб. Он идёт от 1 до 3, + // потому что камеры смотрят вниз по -Z, поэтому мы хотим, + // чтобы камера начиналась с Z = 0. + // Поместим конус перед этим кубом, открытый + // в сторону -Z + const positions = [ + -1, -1, 1, // вершины куба + 1, -1, 1, + -1, 1, 1, + 1, 1, 1, + -1, -1, 3, + 1, -1, 3, + -1, 1, 3, + 1, 1, 3, + 0, 0, 1, // вершина конуса + ]; + const indices = [ + 0, 1, 1, 3, 3, 2, 2, 0, // индексы куба + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 3, 7, 2, 6, + ]; + // добавляем сегменты конуса + const numSegments = 6; + const coneBaseIndex = positions.length / 3; + const coneTipIndex = coneBaseIndex - 1; + for (let i = 0; i < numSegments; ++i) { + const u = i / numSegments; + const angle = u * Math.PI * 2; + const x = Math.cos(angle); + const y = Math.sin(angle); + positions.push(x, y, 0); + // линия от вершины к краю + indices.push(coneTipIndex, coneBaseIndex + i); + // линия от точки на краю к следующей точке на краю + indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments); + } + positions.forEach((v, ndx) => { + positions[ndx] *= scale; + }); + return twgl.createBufferInfoFromArrays(gl, { + position: positions, + indices, + }); +} +``` + +Наша текущая программа шейдеров рисует с цветами вершин. Сделаем ещё одну, которая рисует сплошным цветом. + +```js +const colorVS = `#version 300 es +in vec4 a_position; + +uniform mat4 u_matrix; + +void main() { + // Умножаем позицию на матрицу. + gl_Position = u_matrix * a_position; +} +`; + +const colorFS = `#version 300 es +precision highp float; + +uniform vec4 u_color; + +out vec4 outColor; + +void main() { + outColor = u_color; +} +`; +``` + +Теперь используем их для рисования одной сцены с камерой, смотрящей на другую сцену: + +```js +// настройка GLSL программ +// компилирует шейдеры, линкует программу, находит локации +const vertexColorProgramInfo = twgl.createProgramInfo(gl, [vs, fs]); +const solidColorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS]); + +// создаём буферы и заполняем данными для 3D 'F' +const fBufferInfo = twgl.primitives.create3DFBufferInfo(gl); +const fVAO = twgl.createVAOFromBufferInfo(gl, vertexColorProgramInfo, fBufferInfo); + +const cameraScale = 20; +const cameraBufferInfo = createCameraBufferInfo(gl, cameraScale); +const cameraVAO = twgl.createVAOFromBufferInfo( + gl, solidColorProgramInfo, cameraBufferInfo); + +const settings = { + rotation: 150, // в градусах + cam1FieldOfView: 60, // в градусах + cam1PosX: 0, + cam1PosY: 0, + cam1PosZ: -200, +}; + + +function render() { + twgl.resizeCanvasToDisplaySize(gl.canvas); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.SCISSOR_TEST); + + // разделим вид на 2 части + const effectiveWidth = gl.canvas.clientWidth / 2; + const aspect = effectiveWidth / gl.canvas.clientHeight; + const near = 1; + const far = 2000; + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(degToRad(settings.cam1FieldOfView), aspect, near, far); + + // Вычисляем матрицу камеры используя look at. + const cameraPosition = [ + settings.cam1PosX, + settings.cam1PosY, + settings.cam1PosZ, + ]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + const cameraMatrix = m4.lookAt(cameraPosition, target, up); + + let worldMatrix = m4.yRotation(degToRad(settings.rotation)); + worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation)); + // центрируем 'F' вокруг её начала + worldMatrix = m4.translate(worldMatrix, -35, -75, -5); + + const {width, height} = gl.canvas; + const leftWidth = width / 2 | 0; + + // рисуем слева с ортографической камерой + gl.viewport(0, 0, leftWidth, height); + gl.scissor(0, 0, leftWidth, height); + gl.clearColor(1, 0.8, 0.8, 1); + + drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix); + + // рисуем справа с перспективной камерой + const rightWidth = width - leftWidth; + gl.viewport(leftWidth, 0, rightWidth, height); + gl.scissor(leftWidth, 0, rightWidth, height); + gl.clearColor(0.8, 0.8, 1, 1); + + // вычисляем вторую матрицу проекции и вторую камеру + const perspectiveProjectionMatrix2 = + m4.perspective(degToRad(60), aspect, near, far); + + // Вычисляем матрицу камеры используя look at. + const cameraPosition2 = [-600, 400, -400]; + const target2 = [0, 0, 0]; + const cameraMatrix2 = m4.lookAt(cameraPosition2, target2, up); + + drawScene(perspectiveProjectionMatrix2, cameraMatrix2, worldMatrix); + + // рисуем объект для представления первой камеры + { + // Создаём view matrix из матрицы второй камеры. + const viewMatrix = m4.inverse(cameraMatrix2); + + let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix); + // используем матрицу первой камеры как матрицу для позиционирования + // представителя камеры в сцене + mat = m4.multiply(mat, cameraMatrix); + + gl.useProgram(solidColorProgramInfo.program); + + // ------ Рисуем представление камеры -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(cameraVAO); + + // Устанавливаем uniforms + twgl.setUniforms(solidColorProgramInfo, { + u_matrix: mat, + u_color: [0, 0, 0, 1], + }); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, cameraBufferInfo, gl.LINES); + } +} +render(); +``` + +И теперь мы можем видеть камеру, используемую для рендера левой сцены, в сцене справа. + +{{{example url="../webgl-visualize-camera.html"}}} + +Давайте также нарисуем что-то для представления frustum камеры. + +Поскольку frustum представляет преобразование в clip space, мы можем сделать куб, представляющий clip space, +и использовать обратную матрицу проекции для размещения его в сцене. + +Сначала нужен куб линий clip space: + +```js +function createClipspaceCubeBufferInfo(gl) { + // сначала добавим куб. Он идёт от 1 до 3, + // потому что камеры смотрят вниз по -Z, поэтому мы хотим, + // чтобы камера начиналась с Z = 0. Поместим + // конус перед этим кубом, открытый + // в сторону -Z + const positions = [ + -1, -1, -1, // вершины куба + 1, -1, -1, + -1, 1, -1, + 1, 1, -1, + -1, -1, 1, + 1, -1, 1, + -1, 1, 1, + 1, 1, 1, + ]; + const indices = [ + 0, 1, 1, 3, 3, 2, 2, 0, // индексы куба + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 3, 7, 2, 6, + ]; + return twgl.createBufferInfoFromArrays(gl, { + position: positions, + indices, + }); +} +``` + +Затем можем создать один и нарисовать его: + +```js +const cameraScale = 20; +const cameraBufferInfo = createCameraBufferInfo(gl, cameraScale); +const cameraVAO = twgl.createVAOFromBufferInfo( + gl, solidColorProgramInfo, cameraBufferInfo); + +const clipspaceCubeBufferInfo = createClipspaceCubeBufferInfo(gl); +const clipspaceCubeVAO = twgl.createVAOFromBufferInfo( + gl, solidColorProgramInfo, clipspaceCubeBufferInfo); + + // рисуем объект для представления первой камеры + { + // Создаём view matrix из матрицы камеры. + const viewMatrix = m4.inverse(cameraMatrix2); + + let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix); + // используем матрицу первой камеры как матрицу для позиционирования + // представителя камеры в сцене + mat = m4.multiply(mat, cameraMatrix); + + gl.useProgram(solidColorProgramInfo.program); + + // ------ Рисуем представление камеры -------- + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(cameraVAO); + + // Устанавливаем uniforms + twgl.setUniforms(solidColorProgramInfo, { + u_matrix: mat, + u_color: [0, 0, 0, 1], + }); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, cameraBufferInfo, gl.LINES); + + // ----- Рисуем frustum ------- + + mat = m4.multiply(mat, m4.inverse(perspectiveProjectionMatrix)); + + // Настраиваем все нужные атрибуты. + gl.bindVertexArray(clipspaceCubeVAO); + + // Устанавливаем uniforms + twgl.setUniforms(solidColorProgramInfo, { + u_matrix: mat, + u_color: [0, 0, 0, 1], + }); + + // вызывает gl.drawArrays или gl.drawElements + twgl.drawBufferInfo(gl, clipspaceCubeBufferInfo, gl.LINES); + } +} +``` + +Давайте также сделаем так, чтобы можно было настраивать near и far параметры первой камеры: + +```js +const settings = { + rotation: 150, // в градусах + cam1FieldOfView: 60, // в градусах + cam1PosX: 0, + cam1PosY: 0, + cam1PosZ: -200, + cam1Near: 30, + cam1Far: 500, +}; + +... + + // Вычисляем матрицу перспективной проекции + const perspectiveProjectionMatrix = + m4.perspective(degToRad(settings.cam1FieldOfView), + aspect, + settings.cam1Near, + settings.cam1Far); +``` + +и теперь мы можем видеть frustum тоже: + +{{{example url="../webgl-visualize-camera-with-frustum.html"}}} + +Если вы настроите near или far плоскости или поле зрения так, чтобы они обрезали F, вы увидите, +что представление frustum совпадает. + +Будем ли мы использовать перспективную или ортографическую проекцию для камеры слева — это будет работать в любом случае, +потому что матрица проекции всегда преобразует в clip space, поэтому её обратная всегда возьмёт наш куб от +1 до -1 +и исказит его соответствующим образом. + +```js +const settings = { + rotation: 150, // в градусах + cam1FieldOfView: 60, // в градусах + cam1PosX: 0, + cam1PosY: 0, + cam1PosZ: -200, + cam1Near: 30, + cam1Far: 500, + cam1Ortho: true, + cam1OrthoUnits: 120, +}; + +... + +// Вычисляем матрицу проекции +const perspectiveProjectionMatrix = settings.cam1Ortho + ? m4.orthographic( + -settings.cam1OrthoUnits * aspect, // left + settings.cam1OrthoUnits * aspect, // right + -settings.cam1OrthoUnits, // bottom + settings.cam1OrthoUnits, // top + settings.cam1Near, + settings.cam1Far) + : m4.perspective(degToRad(settings.cam1FieldOfView), + aspect, + settings.cam1Near, + settings.cam1Far); +``` + +{{{example url="../webgl-visualize-camera-with-orthographic.html"}}} \ No newline at end of file diff --git a/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md b/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md new file mode 100644 index 000000000..f5ca949f4 --- /dev/null +++ b/webgl/lessons/ru/webgl1-to-webgl2-fundamentals.md @@ -0,0 +1,80 @@ +Title: Отличия от WebGLFundamentals.org +Description: Различия между WebGLFundamentals.org и WebGL2Fundamentals.org +TOC: Отличия от WebGLFundamentals.org до WebGL2Fundamentals.org + +Если вы ранее читали [webglfundamentals.org](https://webglfundamentals.org), +есть некоторые различия, о которых стоит знать. + +## Многострочные шаблонные литералы + +На webglfundamentals.org почти все скрипты хранятся +в не-javascript тегах `; + + ... + + var vertexShaderSource = document.querySelector("#vertexshader").text; + +На webgl2fundamentals.org я перешёл на использование +многострочных шаблонных литералов. + + var vertexShaderSource = ` + шейдер + здесь + `; + +Многострочные шаблонные литералы поддерживаются во всех браузерах с WebGL, +кроме IE11. Если нужно поддерживать IE11, рассмотрите использование +транспайлера типа [babel](https://babeljs.io). + +## Все шейдеры используют версию GLSL 300 es + +Я перевёл все шейдеры на GLSL 300 es. Я подумал, в чём смысл +использовать WebGL2, если не использовать шейдеры WebGL2. + +## Все примеры используют Vertex Array Objects + +Vertex Array Objects — это опциональная функция в WebGL1, но +стандартная функция WebGL2. [Я думаю, их следует использовать везде](webgl1-to-webgl2.html#Vertex-Array-Objects). +Фактически я почти думаю, что стоит вернуться +к webglfundamentals.org и использовать их везде [с помощью +полифилла](https://github.com/greggman/oes-vertex-array-object-polyfill) +для тех немногих мест, где они недоступны. Аргументированно нулевой +недостаток, а ваш код становится проще и эффективнее почти +во всех случаях. + +## Другие мелкие изменения + +* Я попытался немного переструктурировать многие примеры, чтобы показать наиболее распространённые паттерны. + + Например, большинство приложений обычно устанавливают глобальное состояние WebGL типа blending, culling и depth testing + в их рендер-цикле, поскольку эти настройки часто меняются несколько раз, тогда как на + webglfundamentals.org я устанавливал их при инициализации, потому что они нужны были + только один раз, но это не распространённый паттерн. + +* Я устанавливаю viewport во всех примерах + + Я пропускал это на webglfundamentals.org, потому что примеры + не нуждаются в этом, но это нужно почти во всём реальном коде. + +* Я убрал jquery. + + Когда я начинал, `` возможно ещё не был широко + поддерживаем, но теперь поддерживается везде. + +* Я сделал префикс для всех вспомогательных функций + + Код типа + + var program = createProgramFromScripts(...) + + теперь + + webglUtils.createProgramFromSources(...); + + Я надеюсь, это делает более ясным, что это за функции + и где их найти. \ No newline at end of file diff --git a/webgl/lessons/ru/webgl1-to-webgl2.md b/webgl/lessons/ru/webgl1-to-webgl2.md new file mode 100644 index 000000000..eced4e05a --- /dev/null +++ b/webgl/lessons/ru/webgl1-to-webgl2.md @@ -0,0 +1,200 @@ +Title: WebGL2 из WebGL1 +Description: Как перейти с WebGL1 на WebGL2 +TOC: Переход с WebGL1 на WebGL2 + + +WebGL2 **почти** на 100% обратно совместим с WebGL1. +Если вы используете только функции WebGL1, то есть только +2 **основных** различия. + +1. Вы используете `"webgl2"` вместо `"webgl"` при вызове `getContext`. + + var gl = someCanvas.getContext("webgl2"); + + Примечание: нет "experimental-webgl2". Производители браузеров собрались + вместе и решили не продолжать префиксовать вещи, потому что веб-сайты + становятся зависимыми от префикса. + +2. Многие расширения являются стандартной частью WebGL2 и поэтому недоступны + как расширения. + + Например, объекты вершинных массивов `OES_vertex_array_object` являются + стандартной функцией WebGL2. Так, например, в WebGL1 вы бы делали это + + var ext = gl.getExtension("OES_vertex_array_object"); + if (!ext) { + // сказать пользователю, что у него нет требуемого расширения или обойти это + } else { + var someVAO = ext.createVertexArrayOES(); + } + + В WebGL2 вы бы делали это + + var someVAO = gl.createVertexArray(); + + Потому что это просто существует. + +Тем не менее, чтобы воспользоваться большинством функций WebGL2, вам нужно будет внести +некоторые изменения. + +## Переход на GLSL 300 es + +Самое большое изменение - вы должны обновить ваши шейдеры до GLSL 3.00 ES. Для этого +первая строка ваших шейдеров должна быть + + #version 300 es + +**ПРИМЕЧАНИЕ: ЭТО ДОЛЖНО БЫТЬ ПЕРВОЙ СТРОКОЙ! Никаких комментариев и пустых строк перед ней не допускается.** + +Другими словами, это плохо + + // ПЛОХО!!!! +---Здесь есть новая строка! + // ПЛОХО!!!! V + var vertexShaderSource = ` + #version 300 es + .. + `; + +Это тоже плохо + +