Это продолжение статьи об обработке изображений в WebGL. Если вы её ещё не читали - предлагаю ознакомиться сначала с ней.
Наиболее очевидный вопрос по обработке изображений - как применить несколько эффектов?
Можно попробовать создавать шейдеры на лету. Создать интерфейс, который позволит пользователю выбрать требуемые эффекты и затем создать шейдер, который создаст все эффекты. Такой подход не всегда возможен, однако он часто используется для создания эффектов в графике в реальном времени.
Более гибкий способ - использовать 2 и более текстуры и отрисовывать по очереди каждый пиксель, переключаясь между текстурами туда и обратно и применяя каждый раз следующий эффект.
Оригинал -> [Blur] -> Текстура 1 Текстура 1 -> [Резкость] -> Текстура 2 Текстура 2 -> [Выделение границ] -> Текстура 1 Текстура 1 -> [Размытие] -> Текстура 2 Текстура 2 -> [Нормальное] -> Canvas
Для такой реализации нам понадобятся фреймбуферы. В WebGL и OpenGL фреймбуфер является не совсем тем, чем называется. Фреймбуфер WebGL/OpenGL - это просто набор состояний и на самом деле никакой не буфер. Но, прикрепив текстуру к фреймбуферу, мы можем провести рендеринг в эту текстуру.
Сначала преобразуем старый код создания текстуры в функцию
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
А теперь используем эту функцию для создания ещё 2 текстур и прикрепления их к 2 фреймбуферам.
// создаём 2 текстуры и прикрепления их к фреймбуферам
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
var texture = createAndSetupTexture(gl);
textures.push(texture);
// задаём размер текстуры равным размеру изображения
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);
// создаём фреймбуфер
var fbo = gl.createFramebuffer();
framebuffers.push(fbo);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// и прикрепляем к нему текстуру
gl.framebufferTexture2D(
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
Теперь создадим набор ядер и зададим порядок, в котором они будут применяться
// определяем несколько ядер свёртки
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"
];
И, наконец, применим каждый эффект, меняя текстуру, в которую идёт отрисовка.
// начинаем с оригинального изображения
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);
// не переворачиваем изображение при отрисовке в текстуру
gl.uniform1f(flipYLocation, 1);
// цикл по каждому эффекту, который мы хотим применить
for (var ii = 0; ii < effectsToApply.length; ++ii) {
// устанавливаем отрисовку в один из фреймбуферов
setFramebuffer(framebuffers[ii % 2], image.width, image.height);
drawWithKernel(effectsToApply[ii]);
// для следующей отрисовки используем текстуру,
// куда только что происходил рендеринг
gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}
// выводим конечный результат на canvas
gl.uniform1f(flipYLocation, -1); // здесь уже нужно перевернуть y координату
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");
function setFramebuffer(fbo, width, height) {
// назначить активный фреймбуфер, куда идёт рендеринг
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// сообщаем шейдеру размеры фреймбуфера
gl.uniform2f(resolutionLocation, width, height);
// указываем настройки окна для фреймбуфера
gl.viewport(0, 0, width, height);
}
function drawWithKernel(name) {
// задаём ядро
gl.uniform1fv(kernelLocation, kernels[name]);
// отрисовываем прямоугольник
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
Вот рабочая версия с немного более гибким интерфейсом. Отметьте галочку возле эффекта, который хотите включить. Потяните эффект, чтобы изменить порядок, в котором они применяются.
Теперь о некоторых вещах подробнее.
Вызывая gl.bindFramebuffer
со значением параметра null
, мы говорим WebGL, что хотим отрисовать в канвас, а не в один из фреймбуферов.
Для WebGL необходима конвертация из пространства отсечения в пиксели. Это осуществляется через установку gl.viewport
, значение по умолчанию которого равно размеру канваса, которое мы установили при инициализации WebGL. Так как размер фреймбуфера, в который происходит отрисовка, отличается от размера canvas, нам необходимо установить viewport соответствующим образом.
Наконец, в исходном примере мы переворачивали координату Y при отрисовке, потому что у WebGL координаты 0,0 находятся в нижнем левом углу canvas, а не в верхнем левом, что более привычно для 2D. Переворот не нужен при отрисовке во фреймбуфер. Так как фреймбуфер никогда не отображается, нам не важно, что является низом, а что верхом. Что имеет значение, так это то, что пиксель 0,0 во фреймбуфере соответствует значению 0,0 в наших вычислениях. Для того, чтобы управлять переворотом, я добавил ещё одну входную переменную в шейдер.
<script id="vertex-shader-2d" type="x-shader/x-vertex">
...
uniform float u_flipY;
...
void main() {
...
gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
...
}
</script>
И теперь при рендеринге мы можем её устанавливать
...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// не переворачиваем
gl.uniform1f(flipYLocation, 1);
...
// переворачиваем
gl.uniform1f(flipYLocation, -1);
Для простоты этого примера я использовал одну программу, чтобы добиться нескольких эффектов. Если вам нужна полноценная обработка изображений, вам, вероятно, понадобится множество программ GLSL. Одна программа для цветового тона, насыщенности и регулировки яркости. Другая для яркости и контраста. Ещё для инверсии, для настройки уровней и так далее. Также понадобилось бы изменить код для переключения между GLSL-программами и обновления параметров для той или иной программы. Я бы рассмотрел написание такого примера, но это упражнение больше подходит читателю, так как множество программ GLSL, каждая со своими параметрами, требуют серьёзного рефакторинга кода, чтобы он не превратился в спагетти.
Надеюсь, что этот и предыдущий пример сделал WebGL немного ближе к вам, а также надеюсь, что разбор примеров в 2D-пространстве упрощает понимание WebGL. Если найду время, то подготовлю ещё несколько статей о том, как сделать 3D и более детально рассмотрю, как на самом деле работает WebGL. А пока советую посмотреть, как использовать 2 и более текстуры.