Обработка изображений в WebGL? Легко! Насколько легко? Читаем ниже.
Это продолжение Основ WebGL. Если вы её ещё не читали - предлагаю ознакомиться сначала с ней.
Для отображения изображений в WebGL нам нужно использовать текстуры. При чтении текстуры WebGL ожидает текстурные координаты, по аналогии с тем, как он ожидает координаты пространства отсечения вместо пикселей. Текстурные координаты занимают диапазон от 0.0 до 1.0, независимо от размеров текстуры.
Так как мы отрисовываем один прямоугольник (ну, вообще 2 треугольника), нам нужно указать WebGL, за какой фрагмент текстуры отвечает каждая точка прямоугольника. Мы передадим эту информацию из вершинного шейдера фрагментному шейдеру, используя специальную varying-переменную. Она называется varying, потому что она варьируется. WebGL интерполирует переданные нами значения в вершинный шейдер, когда будет отрисовывать каждый пиксель во фрагментном шейдере.
Мы используем вершинный шейдер из окончания предыдущей статьи, плюс добавим атрибут для передачи текстурных координат, которые в дальнейшем попадут во фрагментный шейдер.
attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;
void main() {
   ...
   // Передаём texCoord фрагментному шейдеру.
   // Видеокарта интерполирует это значение между точками.
   v_texCoord = a_texCoord;
}
Далее говорим фрагментному шейдеру, чтобы он брал цвета из текстуры.
<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;
// наша текстура
uniform sampler2D u_image;
// texCoords, переданные из вершинного шейдера
varying vec2 v_texCoord;
void main() {
   // получение цвета из текстуры
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>
Заключительным шагом будет загрузка изображения, создание текстуры и копирование изображения в текстуру. Из-за того, что браузер грузит изображения асинхронно, нам нужно немного изменить наш код, чтобы дожидаться загрузки текстуры. Как только текстура загрузится, мы отрисуем её.
function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // ДОЛЖНА НАХОДИТЬСЯ НА ТОМ ЖЕ ДОМЕНЕ!!!
  image.onload = function() {
    render(image);
  }
}
function render(image) {
  ...
  // наш код из предыдущего примера
  ...
  // ссылка на атрибут, куда пойдут координаты текстуры
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
  // указываем координаты текстуры для прямоугольника
  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(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
  // создаём текстуру
  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);
  // загружаем изображение в текстуру
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}
И пример отображения картинки в WebGL. ВНИМАНИЕ: если вы запускаете этот пример на локальном компьютере, вам понадобится какой-нибудь веб-сервер, чтобы загружать изображения. Узнайте, как его можно установить за пару минут.
Пока довольно банально, нужно что-то сделать с этим изображением. Как насчёт поменять местами красный и синий?
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
Теперь красный и синий цвет поменялись местами.
А что если нам нужна обработка изображений, где мы принимаем в расчёт другие пиксели? Так как в WebGL идёт обращение к текстуре через текстурные координаты, которые от 0.0 до 1.0, мы можем вычислить, сколько занимает 1 пиксель через несложную математику onePixel = 1.0 / textureSize.
Вот пример фрагментного шейдера, который берёт среднее значение каждого пикселя текстуры и его левого и правого соседа:
<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;
// наша текстура
uniform sampler2D u_image;
uniform vec2 u_textureSize;
// texCoords передаются из вершинного шейдера
varying vec2 v_texCoord;
void main() {
   // рассчитываем один пиксель в текстурных координатах
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   // среднее между средним, правым и левым пикселем
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>
Ещё нам нужно передать размер текстуры из JavaScript.
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// задаём размер изображения
gl.uniform2f(textureSizeLocation, image.width, image.height);
...
Сравните с оригинальным изображением выше.
Итак, теперь мы знаем, как брать значение других пикселей, теперь применим ядро свёртки, чтобы выполнить распространённые методы обработки изображений. Мы будем использовать ядро 3x3. Ядро свёртки - это просто матрица 3х3, где каждое значение в матрице означает, на сколько умножить 8 пикселей, находящихся вокруг пикселя, который мы отрисовываем. Затем мы делим результат на сумму всех значений ядра или на 1, смотря что больше. Вот довольно неплохая статья на этот счёт. А вот ещё одна статья, показывающая, как написать код, задающий ядро свёртки на C++.
В нашем случае мы планируем выполнить ту же работу в шейдере, поэтому вот код фрагментного шейдера.
<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;
// наша текстура
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
uniform float u_kernelWeight;
// texCoords, переданные из вершинного шейдера
varying vec2 v_texCoord;
void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   // делим сумму на весовой коэффициент, но берём из результата только rgb
   // прозрачность установим в значение 1.0
   gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0);
}
</script>
В JavaScript нужно задать ядро свёртки и делитель
 function computeKernelWeight(kernel) {
   var weight = kernel.reduce(function(prev, curr) {
       return prev + curr;
   });
   return weight <= 0 ? 1 : weight;
 }
 ...
 var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
 var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
 ...
 var edgeDetectKernel = [
     -1, -1, -1,
     -1,  8, -1,
     -1, -1, -1
 ];
 gl.uniform1fv(kernelLocation, edgeDetectKernel);
 gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
 ...
И вуаля! Используйте выпадающий список для выбора различных ядер.
Надеюсь, эта статья убедила вас, что обработка изображений в WebGL - это довольно просто. Дальше я рассмотрю, как применить к изображению более одного эффекта.