Обработка изображений в 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 - это довольно просто. Дальше я рассмотрю, как применить к изображению более одного эффекта.