From ad75af0ad4884c40e78694a04a228f89d62b444e Mon Sep 17 00:00:00 2001
From: vanzo16-github <141827057+vanzo16-github@users.noreply.github.com>
Date: Wed, 3 Apr 2024 13:08:02 +0300
Subject: [PATCH] Create webgl-ramp-textures.md
yet another translation
---
webgl/lessons/ru/webgl-ramp-textures.md | 514 ++++++++++++++++++++++++
1 file changed, 514 insertions(+)
create mode 100644 webgl/lessons/ru/webgl-ramp-textures.md
diff --git a/webgl/lessons/ru/webgl-ramp-textures.md b/webgl/lessons/ru/webgl-ramp-textures.md
new file mode 100644
index 000000000..07e59898c
--- /dev/null
+++ b/webgl/lessons/ru/webgl-ramp-textures.md
@@ -0,0 +1,514 @@
+Title: WebGL Ramp Textures
+Description: Using ramp textures
+TOC: Ramp Textures (Toon Shading)
+
+Важным моментом в WebGL является то, что текстуры — это не просто элементы,
+применяемые непосредственно к треугольникам, как мы рассмотрели в [ статье о текстурах. ](webgl-3d-textures.html).
+Текстуры — это массивы данных произвольного доступа, обычно двумерные массивы данных.
+Таким образом, любое решение, в котором мы могли бы использовать массив данных с произвольным доступом,
+— это место, где мы, вероятно, можем использовать текстуру.
+
+В [статье о направленном освещени](webgl-3d-lighting-directional.html)
+мы рассказали, как использовать *скалярное произведение*для вычисления угла между двумя векторами.
+В нем мы вычислили *скалярное произведение* направления света на нормаль к поверхности нашей модели.
+Так мы определили косинус угла между двумя векторами. Косинус — это значение от -1 до +1,
+и мы использовали его как непосредственный множитель нашего цвета.
+
+
+
+```glsl
+float light = dot(normal, u_reverseLightDirection);
+
+gl_FragColor = u_color;
+gl_FragColor.rgb *= light;
+```
+
+Это затемнит свет.
+
+Что, если вместо непосредственного использования этого скалярного произведения мы воспользуемся им для поиска значения в одномерной текстуре?
+
+```glsl
+precision mediump float;
+
+// Passed in from the vertex shader.
+varying vec3 v_normal;
+
+uniform vec3 u_reverseLightDirection;
+uniform vec4 u_color;
++uniform sampler2D u_ramp;
+
+void main() {
+ // because v_normal is a varying it's interpolated
+ // so it will not be a unit vector. Normalizing it
+ // will make it a unit vector again
+ vec3 normal = normalize(v_normal);
+
+- float light = dot(normal, u_reverseLightDirection);
++ float cosAngle = dot(normal, u_reverseLightDirection);
++
++ // convert from -1 <-> 1 to 0 <-> 1
++ float u = cosAngle * 0.5 + 0.5;
++
++ // make a texture coordinate
++ vec2 uv = vec2(u, 0.5);
++
++ // lookup a value from a 1d texture
++ vec4 rampColor = texture2D(u_ramp, uv);
++
+ gl_FragColor = u_color;
+- gl_FragColor.rgb *= light;
++ gl_FragColor *= rampColor;
+}
+```
+Нам нужно создать текстуру. Начнем с текстуры 2х1. Мы будем использовать формат `LUMINANCE`,
+который дает нам монохромную текстуру, используя только 1 байт на тексель.
+
+```js
+var tex = gl.createTexture();
+gl.bindTexture(gl.TEXTURE_2D, tex);
+gl.texImage2D(
+ gl.TEXTURE_2D, // target
+ 0, // mip level
+ gl.LUMINANCE, // internal format
+ 2, // width
+ 1, // height
+ 0, // border
+ gl.LUMINANCE, // format
+ gl.UNSIGNED_BYTE, // type
+ new Uint8Array([90, 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);
+gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+```
+
+Цвета двух пикселей выше — темно-серый (90) и белый (255).
+Мы также устанавливаем параметры текстуры, чтобы не было фильтрации.
+
+Модифицируя образец новой текстуры, нам нужно найти форму `u_ramp`.
+
+```js
+var worldViewProjectionLocation = gl.getUniformLocation(program, "u_worldViewProjection");
+var worldInverseTransposeLocation = gl.getUniformLocation(program, "u_worldInverseTranspose");
+var colorLocation = gl.getUniformLocation(program, "u_color");
++var rampLocation = gl.getUniformLocation(program, "u_ramp");
+var reverseLightDirectionLocation =
+ gl.getUniformLocation(program, "u_reverseLightDirection");
+```
+
+и нам нужно настроить текстуру при рендеринге
+
+```js
+// bind the texture to active texture unit 0
+gl.activeTexture(gl.TEXTURE0 + 0);
+gl.bindTexture(gl.TEXTURE_2D, tex);
+// tell the shader that u_ramp should use the texture on texture unit 0
+gl.uniform1i(rampLocation, 0);
+```
+
+Я заменил данные для 3D `F` из образца света на данные для "низкополигональной" головы.
+Запустив его, мы получим это
+
+{{{example url="../webgl-ramp-texture.html"}}}
+
+Если вы повернете модель, вы увидите, что она похожа на [toon shading](https://en.wikipedia.org/wiki/Cel_shading)
+
+В приведенном выше примере мы установили фильтрацию текстур на `NEAREST`, что означает,
+что мы просто выбираем ближайший тексел из текстуры для нашего цвета.
+Есть только 2 текселя, поэтому, если поверхность обращена от света, мы получаем первый цвет (темно-серый),
+а если поверхность обращена к свету, мы получаем второй цвет (белый).
+Этот цвет умножается на `gl_FragColor`, точно так же, как раньше был `light`.
+
+Thinking about it if we switch to `LINEAR` filtering we *should* get the same
+result as before using the texture. Let's try it.
+
+Если подумать, если мы переключимся на `LINEAR` фильтрацию,
+*мы должны* получить тот же результат, что и перед использованием текстуры. Давай попробуем.
+
+```js
+-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_MIN_FILTER, gl.LINEAR);
++gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+```
+
+{{{example url="../webgl-ramp-texture-linear.html"}}}
+
+Это выглядит похоже, но если мы действительно сравним их друг с другом...
+
+
+
+Мы видим, что они не одинаковы. Но почему?
+
+`LINEAR` фильтрация смешивает пиксели.
+Если мы увеличим текстуру размером 2 пикселя с линейной фильтрацией, мы увидим проблему.
+
+
+Диапазон координат текстуры для ramp
+
+С каждой стороны по полпикселя без интерполяции. Представьте, если для текстуры было установлено
+`TEXTURE_WRAP_S` в `REPEAT`. Тогда мы ожидаем, что самая левая половина красного пикселя будет линейно сливаться с зеленым,
+как если бы зеленый цвет повторялся влево. Но то, что слева, более красное, поскольку мы используем `CLAMP_TO_EDGE`.
+
+Чтобы действительно получить линейное изменение, нам просто нужно выбрать значения из этого центрального диапазона.
+Мы можем сделать это с помощью небольших математических вычислений в нашем шейдере.
+
+```glsl
+precision mediump float;
+
+// Passed in from the vertex shader.
+varying vec3 v_normal;
+
+uniform vec3 u_reverseLightDirection;
+uniform vec4 u_color;
+uniform sampler2D u_ramp;
++uniform vec2 u_rampSize;
+
+void main() {
+ // because v_normal is a varying it's interpolated
+ // so it will not be a unit vector. Normalizing it
+ // will make it a unit vector again
+ vec3 normal = normalize(v_normal);
+
+ float cosAngle = dot(normal, u_reverseLightDirection);
+
+ // convert from -1 <-> 1 to 0 <-> 1
+ float u = cosAngle * 0.5 + 0.5;
+
+ // make a texture coordinate.
+ vec2 uv = vec2(u, 0.5);
+
++ // scale to size of ramp
++ vec2 texelRange = uv * (u_rampSize - 1.0);
++
++ // offset by half a texel and convert to texture coordinate
++ vec2 rampUV = (texelRange + 0.5) / u_rampSize;
+
+- vec4 rampColor = texture2D(u_ramp, uv);
++ vec4 rampColor = texture2D(u_ramp, rampUV);
+
+ gl_FragColor = u_color;
+ gl_FragColor *= rampColor;
+}
+```
+
+Выше мы в основном масштабируем нашу UV-координату, чтобы она изменялась от 0 до 1 на 1 меньше ширины текстуры.
+Затем добавляем половину пикселя и конвертируем обратно в нормализованные координаты текстуры.
+
+Нам нужно найти местоположение `u_rampSize`
+
+```js
+var colorLocation = gl.getUniformLocation(program, "u_color");
+var rampLocation = gl.getUniformLocation(program, "u_ramp");
++var rampSizeLocation = gl.getUniformLocation(program, "u_rampSize");
+```
+
+И нам нужно установить его во время рендеринга
+
+```js
+// bind the texture to active texture unit 0
+gl.activeTexture(gl.TEXTURE0 + 0);
+gl.bindTexture(gl.TEXTURE_2D, tex);
+// tell the shader that u_ramp should use the texture on texture unit 0
+gl.uniform1i(rampLocation, 0);
++gl.uniform2fv(rampSizeLocation, [2, 1]);
+```
+
+Прежде чем запустить его, давайте добавим флаг, чтобы мы могли сравнивать с *ramp texture* и без нее.
+
+
+```glsl
+precision mediump float;
+
+// Passed in from the vertex shader.
+varying vec3 v_normal;
+
+uniform vec3 u_reverseLightDirection;
+uniform vec4 u_color;
+uniform sampler2D u_ramp;
+uniform vec2 u_rampSize;
++uniform bool u_useRampTexture;
+
+void main() {
+ // because v_normal is a varying it's interpolated
+ // so it will not be a unit vector. Normalizing it
+ // will make it a unit vector again
+ vec3 normal = normalize(v_normal);
+
+ float cosAngle = dot(normal, u_reverseLightDirection);
+
+ // convert from -1 <-> 1 to 0 <-> 1
+ float u = cosAngle * 0.5 + 0.5;
+
+ // make a texture coordinate.
+ vec2 uv = vec2(u, 0.5);
+
+ // scale to size of ramp
+ vec2 texelRange = uv * (u_rampSize - 1.0);
+
+ // offset by half a texel and convert to texture coordinate
+ vec2 rampUV = (texelRange + 0.5) / u_rampSize;
+
+ vec4 rampColor = texture2D(u_ramp, rampUV);
+
++ if (!u_useRampTexture) {
++ rampColor = vec4(u, u, u, 1);
++ }
+
+ gl_FragColor = u_color;
+ gl_FragColor *= rampColor;
+}
+```
+
+Мы также найдем местонахождение этой формы.
+
+```js
+var rampLocation = gl.getUniformLocation(program, "u_ramp");
+var rampSizeLocation = gl.getUniformLocation(program, "u_rampSize");
++var useRampTextureLocation = gl.getUniformLocation(program, "u_useRampTexture");
+```
+
+и установим её
+
+```js
+var data = {
+ useRampTexture: true,
+};
+
+...
+
+// bind the texture to active texture unit 0
+gl.activeTexture(gl.TEXTURE0 + 0);
+gl.bindTexture(gl.TEXTURE_2D, tex);
+// tell the shader that u_ramp should use the texture on texture unit 0
+gl.uniform1i(rampLocation, 0);
+gl.uniform2fv(rampSizeLocation, [2, 1]);
+
++gl.uniform1i(useRampTextureLocation, data.useRampTexture);
+```
+
+Таким образом, мы видим, что старый способ освещения и новый способ texture rump совпадают.
+
+{{{example url="../webgl-ramp-texture-issue-confirm.html"}}}
+
+Установив флажок «useRampTexture», мы не видим никаких изменений, поскольку теперь эти два метода совпадают.
+
+> Примечание. Обычно я не рекомендую использовать в шейдере условие типа `u_useRampTexture`
+> Вместо этого я рекомендую создать две шейдерные программы: одну, использующую нормальное освещение,
+> и другую, использующую ramp texture. К сожалению, поскольку код не использует
+> что-то вроде [нашей вспомогательной библиотеки](webgl-less-code-more-fun.html),
+> для поддержки двух шейдерных программ потребовалось бы существенное изменение.
+> Для каждой программы нужен свой набор локаций. Внесение столь значительных изменений отвлекло
+> бы от сути этой статьи, поэтому в данном случае я решил использовать условность.
+> В целом я стараюсь избегать условий при выборе функций в шейдерах и вместо этого создаю разные шейдеры для разных функций.
+
+Примечание. Эта математика важна только в том случае, если мы используем `LINEAR` фильтрацию.
+Если мы используем фильтрацию `NEAREST`, нам нужна исходная математика.
+
+Теперь, когда мы знаем, что математические расчеты рампы верны, давайте создадим несколько различных ramp textures.
+
+```js
++// make a 256 array where elements 0 to 127
++// go from 64 to 191 and elements 128 to 255
++// are all 255.
++const smoothSolid = new Array(256).fill(255);
++for (let i = 0; i < 128; ++i) {
++ smoothSolid[i] = 64 + i;
++}
++
++const ramps = [
++ { name: 'dark-white', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 255] },
++ { name: 'dark-white-skewed', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 80, 80, 255, 255] },
++ { name: 'normal', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: true,
++ data: [0, 255] },
++ { name: '3-step', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 160, 255] },
++ { name: '4-step', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 140, 200, 255] },
++ { name: '4-step skewed', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 80, 80, 80, 140, 200, 255] },
++ { name: 'black-white-black', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 255, 80] },
++ { name: 'stripes', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255, 80, 255] },
++ { name: 'stripe', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: [80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] },
++ { name: 'smooth-solid', color: [0.2, 1, 0.2, 1], format: gl.LUMINANCE, filter: false,
++ data: smoothSolid },
++ { name: 'rgb', color: [ 1, 1, 1, 1], format: gl.RGB, filter: true,
++ data: [255, 0, 0, 0, 255, 0, 0, 0, 255] },
++];
++
++var elementsForFormat = {};
++elementsForFormat[gl.LUMINANCE] = 1;
++elementsForFormat[gl.RGB ] = 3;
++
++ramps.forEach((ramp) => {
++ const {name, format, filter, data} = ramp;
+ var tex = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, tex);
++ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
++ const width = data.length / elementsForFormat[format];
+ gl.texImage2D(
+ gl.TEXTURE_2D, // target
+ 0, // mip level
+* format, // internal format
+* width,
+ 1, // height
+ 0, // border
+* format, // format
+ gl.UNSIGNED_BYTE, // type
+* new Uint8Array(data));
+ 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, filter ? gl.LINEAR : gl.NEAREST);
+* gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter ? gl.LINEAR : gl.NEAREST);
++ ramp.texture = tex;
++ ramp.size = [width, 1];
++});
+```
+
+и давайте создадим шейдер так, чтобы он мог обрабатывать как `NEAREST, так и `LINEAR`.
+Как я уже упоминал выше, я обычно не использую логические операторы if в шейдерах,
+но если различия небольшие и я могу сделать это без условия, то я рассмотрю возможность использования одного шейдера.
+Для этого мы можем добавить плавающую форму `u_linearAdjust`, которой мы установим значение 0,0 или 1,0.
+
+```glsl
+precision mediump float;
+
+// Passed in from the vertex shader.
+varying vec3 v_normal;
+
+uniform vec3 u_reverseLightDirection;
+uniform vec4 u_color;
+uniform sampler2D u_ramp;
+uniform vec2 u_rampSize;
+-uniform bool u_useRampTexture;
+-uniform float u_linearAdjust; // 1.0 if linear, 0.0 if nearest
+
+void main() {
+ // because v_normal is a varying it's interpolated
+ // so it will not be a unit vector. Normalizing it
+ // will make it a unit vector again
+ vec3 normal = normalize(v_normal);
+
+ float cosAngle = dot(normal, u_reverseLightDirection);
+
+ // convert from -1 <-> 1 to 0 <-> 1
+ float u = cosAngle * 0.5 + 0.5;
+
+ // make a texture coordinate.
+ vec2 uv = vec2(u, 0.5);
+
+ // scale to size of ramp
+- vec2 texelRange = uv * (u_rampSize - 1.0);
++ vec2 texelRange = uv * (u_rampSize - u_linearAdjust);
+
+- // offset by half a texel and convert to texture coordinate
+- vec2 rampUV = (texelRange + 0.5) / u_rampSize;
++ // offset by half a texel if linear and convert to texture coordinate
++ vec2 rampUV = (texelRange + 0.5 * u_linearAdjust) / u_rampSize;
+
+ vec4 rampColor = texture2D(u_ramp, rampUV);
+
+- if (!u_useRampTexture) {
+- rampColor = vec4(u, u, u, 1);
+- }
+
+ gl_FragColor = u_color;
+ gl_FragColor *= rampColor;
+}
+```
+
+во время инициализации найдите местоположение
+
+```js
+var colorLocation = gl.getUniformLocation(program, "u_color");
+var rampLocation = gl.getUniformLocation(program, "u_ramp");
+var rampSizeLocation = gl.getUniformLocation(program, "u_rampSize");
++var linearAdjustLocation = gl.getUniformLocation(program, "u_linearAdjust");
+```
+
+и во время рендеринга выберите одну из текстур
+
+```js
+var data = {
+ ramp: 0,
+};
+
+...
++const {texture, color, size, filter} = ramps[data.ramp];
+
+// Set the color to use
+-gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]);
++gl.uniform4fv(colorLocation, color);
+
+// set the light direction.
+gl.uniform3fv(reverseLightDirectionLocation, m4.normalize([-1.75, 0.7, 1]));
+
+// bind the texture to active texture unit 0
+gl.activeTexture(gl.TEXTURE0 + 0);
+-gl.bindTexture(gl.TEXTURE_2D, tex);
++gl.bindTexture(gl.TEXTURE_2D, texture);
+// tell the shader that u_ramp should use the texture on texture unit 0
+gl.uniform1i(rampLocation, 0);
+-gl.uniform2fv(rampSizeLocation, [2, 1]);
++gl.uniform2fv(rampSizeLocation, size);
+
++// adjust if linear
++gl.uniform1f(linearAdjustLocation, filter ? 1 : 0);
+```
+
+{{{example url="../webgl-ramp-textures.html"}}}
+
+Попробуйте различные ramp textures, и вы увидите множество странных эффектов.
+Это один из способов универсальной корректировки шейдера.
+Вы можете создать шейдер, который выполняет двухцветное *toon shading*,
+установив 2 цвета и такой порог.
+
+```js
+uniform vec4 color1;
+uniform vec4 color2;
+uniform float threshold;
+
+...
+
+ float cosAngle = dot(normal, u_reverseLightDirection);
+
+ // convert from -1 <-> 1 to 0 <-> 1
+ float u = cosAngle * 0.5 + 0.5;
+
+ gl_FragColor = mix(color1, color2, step(cosAngle, threshold));
+```
+
+И это сработает. Но если вам нужна трехэтапная или четырехшаговая версия,
+вам придется написать еще один шейдер. С *ramp texture* вы можете просто
+создать другую текстуру. Кроме того, обратите внимание выше: даже если вам
+нужен двухшаговый шейдер мультяшного изображения,
+вы все равно можете настроить место выполнения шага, просто добавив больше
+или меньше данных в текстуру. Например текстура с
+
+```
+[dark, light]
+```
+
+Дает вам двухступенчатую текстуру, где она разделяется посередине между направлением к свету или от него. Но текстура типа
+
+```
+[dark, dark, dark, light, light]
+```
+
+позволит переместить разделение на отметку 60% между лицом от света и лицом к свету, и все это без необходимости изменения шейдера.
+
+Этот конкретный пример использования *ramp texture* для *toon shading* или странных
+эффектов может оказаться для вас полезным, а может и нет, но более важным выводом является
+просто базовая концепция использования некоторого значения для поиска данных в текстуре.
+Использование подобных текстур предназначено не только для преобразования расчета освещения.
+Вы можете использовать *ramp texture* [для постобработки](webgl-post-processing.html), чтобы добиться того же эффекта,
+[что и карта градиента в фотошопе](https://www.photoshopessentials.com/photo-effects/gradient-map/)
+
+Вы также можете использовать *ramp texture* для анимации на основе графического процессора.
+Вы сохраняете ключевые значения в текстуре и используете «time» в качестве значения
+для перемещения по текстуре. Есть много применений этой техники.