Exercise #
- Figure it out the js code of the above sketches.
- Implement other blending modes.
Blending Modes #
Esta aplicación aplica diferentes modos de blend a una imagen con un color. La imagen y el color son seleccionados por el usuario.
Primero se renderiza la imagen cargada, y se envía como textura al blend shader. Al blend shader se le envía el color seleccionado por el usuario como un arreglo de JavaScript de cuatro posiciones. Cada entrada se divide en 255 para representar un color RGBA normalizado [0..1].
El blend shader toma el color de cada texel que recibe en la textura (imagen cargada y recibida) y hace el cálculo del blend con el color (vec4) recibido, utilizando la fórmula según corresponda.
El resultado de la operación de blend es un color que para el pixel de la textura de salida, la cual se renderiza sobre el cuadrado de la parte inferior.
Esa textura renderizada en la parte inferior es la misma imagen cargada al inicio pero sus colores son el resultado de la operación de blend correspondiente texel a texel con el color seleccionado por el usuario.
Los blending modes implementados son: multiply, add (linear dodge), screen, overlay, darkest, lightest, color burn, linear burn, difference, divide, exclusion, color dodge, hard light, vivid light, linear light, pin light t cuatro versiones de soft light: photoshop, pegtop, ilussions.hu y w3c.
Las fórmulas fueron tomadas de Blend Modes (Wikipedia) y Formulas for Photoshop blending modes (RBA’s Astrophotography)
Otras referencias utilizadas: Why my texture coordinates are inverted each time I call my GLSL shader in P5.js (Stack Overflow)
Controles #
- Botón Choose File: para cargar una imagen o video
- Checkbox Default Video: marcar para usar el video por defecto, desmarcar para usar la imagen por defecto
- Slider: controla el brillo
- Select: selecciona el blending mode que desea utilizar
- Color Picker: selecciona el color que se usará en la operación de blend seleccionada para cada texel de la imagen (o video) cargada
Código #
Sketch Code
let blendShader;
let colorB; // picked by user
let B; // vec4 vector sent to shader
let tex; // shader output texture
let cpickerB;
let bslider; // brightness slider
let bmselect; // blending mode select
let brightness;
let mode;
let img; // shader input texture
let input;
let video_on;
function preload() {
blendShader = readShader('/VisualComputing/docs/shaders/fragments/blend.frag', { matrices: Tree.NONE, varyings: Tree.texcoords2 });
}
function setup() {
createCanvas(900, 850, WEBGL);
colorB = color(10, 255, 170);
tex = createGraphics(800, 800, WEBGL);
cpickerB = createColorPicker(colorB);
cpickerB.position(490, 200);
bslider = createSlider(0, 1, 1, 0.05);
bslider.position(490, 120);
bslider.style('width', '80px');
bmselect = createSelect();
bmselect.position(490, 160);
bmselect.option('MULTIPLY', 0);
bmselect.option('ADD (LINEAR DODGE)', 1);
bmselect.option('SCREEN', 2);
bmselect.option('OVERLAY', 3);
bmselect.option('DARKEST', 4);
bmselect.option('LIGHTEST', 5);
bmselect.option('COLOR BURN', 6);
bmselect.option('LINEAR BURN', 7);
bmselect.option('DIFFERENCE', 8);
bmselect.option('DIVIDE', 9);
bmselect.option('EXCLUSION', 10);
bmselect.option('COLOR DODGE', 11);
bmselect.option('HARD LIGHT', 12);
bmselect.option('VIVID LIGHT', 13);
bmselect.option('LINEAR LIGHT', 14);
bmselect.option('PIN LIGHT', 15);
bmselect.option('SOFT LIGHT 1', 16); // photoshop
bmselect.option('SOFT LIGHT 2', 17); // pegtop
bmselect.option('SOFT LIGHT 3', 18); // ilussions.hu
bmselect.option('SOFT LIGHT 4', 19); // w3C
bmselect.selected('MULTIPLY');
img = loadImage('/VisualComputing/docs/shaders/resources/image.jpg');
input = createFileInput(handleFile);
video_on = createCheckbox('default video', false);
video_on.changed(() => {
if (video_on.checked()) {
img = createVideo(['/VisualComputing/docs/shaders/resources/video0.mp4']);
img.hide();
} else {
img = loadImage('/VisualComputing/docs/shaders/resources/image.jpg');
img.hide();
img.pause();
}
blendShader.setUniform('texture', img);
})
}
function draw() {
colorB = cpickerB.color()
background(0);
image(img, -450, -400, 400, 400);
fill(colorB)
square(30, -170, 150);
// vec4 vector sent to shader
B = [colorB._getRed() / 255, colorB._getGreen() / 255, colorB._getBlue() / 255, alpha(colorB) / 255] // normalized
brightness = bslider.value();
mode = bmselect.value();
tex.shader(blendShader)
blendShader.setUniform('texture', img); // each texel will be color A
blendShader.setUniform('colorB', B);
blendShader.setUniform('brightness', brightness);
blendShader.setUniform('mode', mode);
tex.square();
texture(tex);
square(-600, 10, 800);
}
function handleFile(file) {
if (file.type === 'image') {
img = createImg(file.data, '');
img.hide();
}
else if (file.type === 'video') {
img = createVideo([file.data]);
img.hide();
img.loop();
}
}
Blend Shader
precision mediump float;
uniform vec4 colorB;
uniform float brightness; // [0, 1]
uniform int mode;
uniform sampler2D texture;
varying vec2 texcoords2;
void main() {
// https://stackoverflow.com/questions/67576655/why-my-texture-coordinates-are-inverted-each-time-i-call-my-glsl-shader-in-p5js
vec4 colorA = texture2D(texture, vec2(texcoords2.x, 1.0 - texcoords2.y)); // each texel is color A
if (mode == 0) { // multiply
gl_FragColor = colorA * colorB * brightness;
}
else if (mode == 1) { // add (linear dodge)
gl_FragColor = (colorA + colorB) * brightness;
}
else if (mode == 2) { // screen
gl_FragColor = (1. - (1. - colorA) * (1. - colorB)) * brightness;
}
else if (mode == 3) { // overlay
float R = (colorA[0] < 0.5) ? 2. * colorA[0] * colorB[0] : (1. - (1. - colorA[0]) * (1. - colorB[0]));
float G = (colorA[1] < 0.5) ? 2. * colorA[1] * colorB[1] : (1. - (1. - colorA[1]) * (1. - colorB[1]));
float B = (colorA[2] < 0.5) ? 2. * colorA[2] * colorB[2] : (1. - (1. - colorA[2]) * (1. - colorB[2]));
float A = (colorA[3] < 0.5) ? 2. * colorA[3] * colorB[3] : (1. - (1. - colorA[3]) * (1. - colorB[3]));
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 4) { // darkest
gl_FragColor = vec4(min(colorA[0], colorB[0]),
min(colorA[1], colorB[1]),
min(colorA[2], colorB[2]),
min(colorA[3], colorB[3])) * brightness;
}
else if (mode == 5) { // lightest
gl_FragColor = vec4(max(colorA[0], colorB[0]),
max(colorA[1], colorB[1]),
max(colorA[2], colorB[2]),
max(colorA[3], colorB[3])) * brightness;
}
else if (mode == 6) { // color burn
gl_FragColor = 1. - ((1. - colorA) / colorB) * brightness;
}
else if (mode == 7) { // linear burn
gl_FragColor = (colorA + colorB - 1.) * brightness;
}
else if (mode == 8) { // difference
gl_FragColor = (abs(colorA - colorB)) * brightness;
}
else if (mode == 9) { // divide
gl_FragColor = (colorA / colorB) * brightness;
}
else if (mode == 10) { // exclusion
gl_FragColor = (0.5 - 2. * (colorA - 0.5) * (colorB - 0.5)) * brightness;
}
else if (mode == 11) { // color dodge
gl_FragColor = (colorA / (1. - colorB)) * brightness;
}
else if (mode == 12) { // hard light
float R = (colorB[0] > 0.5) ? (1. - (1. - colorA[0]) * (1. - 2. * (colorB[0] - 0.5))) : colorA[0] * 2. * colorB[0];
float G = (colorB[1] > 0.5) ? (1. - (1. - colorA[1]) * (1. - 2. * (colorB[1] - 0.5))) : colorA[1] * 2. * colorB[1];
float B = (colorB[2] > 0.5) ? (1. - (1. - colorA[2]) * (1. - 2. * (colorB[2] - 0.5))) : colorA[2] * 2. * colorB[2];
float A = (colorB[3] > 0.5) ? (1. - (1. - colorA[3]) * (1. - 2. * (colorB[3] - 0.5))) : colorA[3] * 2. * colorB[3];
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 13) { // vivid light
float R = (colorB[0] > 0.5) ? colorA[0] / (1. - 2. * (colorB[0] - 0.5)) : (1. - (1. - colorA[0]) / (2. * colorB[0]));
float G = (colorB[1] > 0.5) ? colorA[1] / (1. - 2. * (colorB[1] - 0.5)) : (1. - (1. - colorA[1]) / (2. * colorB[1]));
float B = (colorB[2] > 0.5) ? colorA[2] / (1. - 2. * (colorB[2] - 0.5)) : (1. - (1. - colorA[2]) / (2. * colorB[2]));
float A = (colorB[3] > 0.5) ? colorA[3] / (1. - 2. * (colorB[3] - 0.5)) : (1. - (1. - colorA[3]) / (2. * colorB[3]));
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 14) { // vivid light
float R = (colorB[0] > 0.5) ? colorA[0] + 2. * (colorB[0] - 0.5) : (colorA[0] + 2. * colorB[0] - 1.);
float G = (colorB[1] > 0.5) ? colorA[1] + 2. * (colorB[1] - 0.5) : (colorA[1] + 2. * colorB[1] - 1.);
float B = (colorB[2] > 0.5) ? colorA[2] + 2. * (colorB[2] - 0.5) : (colorA[2] + 2. * colorB[2] - 1.);
float A = (colorB[3] > 0.5) ? colorA[3] + 2. * (colorB[3] - 0.5) : (colorA[3] + 2. * colorB[3] - 1.);
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 15) { // pin light
float R = (colorB[0] > 0.5) ? max(colorA[0], 2. * (colorB[0] - 0.5)) : min(colorA[0], 2. * colorB[0]);
float G = (colorB[1] > 0.5) ? max(colorA[1], 2. * (colorB[1] - 0.5)) : min(colorA[1], 2. * colorB[1]);
float B = (colorB[2] > 0.5) ? max(colorA[2], 2. * (colorB[2] - 0.5)) : min(colorA[2], 2. * colorB[2]);
float A = (colorB[3] > 0.5) ? max(colorA[3], 2. * (colorB[3] - 0.5)) : min(colorA[3], 2. * colorB[3]);
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 16) { // soft light photoshop
float R = (colorB[0] < 0.5) ? 2. * colorA[0] * colorB[0] + colorA[0] * colorA[0] * (1. - 2. * colorB[0]) : 2. * colorA[0] * (1. - colorB[0]) + sqrt(colorA[0]) * (2. * colorB[0] - 1.);
float G = (colorB[1] < 0.5) ? 2. * colorA[1] * colorB[1] + colorA[1] * colorA[1] * (1. - 2. * colorB[1]) : 2. * colorA[1] * (1. - colorB[1]) + sqrt(colorA[1]) * (2. * colorB[1] - 1.);
float B = (colorB[2] < 0.5) ? 2. * colorA[2] * colorB[2] + colorA[2] * colorA[2] * (1. - 2. * colorB[2]) : 2. * colorA[2] * (1. - colorB[2]) + sqrt(colorA[2]) * (2. * colorB[2] - 1.);
float A = (colorB[3] < 0.5) ? 2. * colorA[3] * colorB[3] + colorA[3] * colorA[3] * (1. - 2. * colorB[3]) : 2. * colorA[3] * (1. - colorB[3]) + sqrt(colorA[3]) * (2. * colorB[3] - 1.);
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 17) { // soft light pegtop
gl_FragColor = ((1. - (2. * colorB)) * (colorA * colorA) + (2. * colorB * colorA)) * brightness;
}
else if (mode == 18) { // soft light illusions.hu
float R = pow(colorA[0], pow(2., 2. * (0.5 - colorB[0])));
float G = pow(colorA[1], pow(2., 2. * (0.5 - colorB[1])));
float B = pow(colorA[2], pow(2., 2. * (0.5 - colorB[2])));
float A = pow(colorA[3], pow(2., 2. * (0.5 - colorB[3])));
gl_FragColor = vec4(R, G, B, A) * brightness;
}
else if (mode == 19) { // soft-light w3c
float gRa = (colorA[0] <= 0.25) ? ((16. * colorA[0] - 12.) * colorA[0] + 4.) * colorA[0] : sqrt(colorA[0]);
float gGa = (colorA[1] <= 0.25) ? ((16. * colorA[1] - 12.) * colorA[1] + 4.) * colorA[1] : sqrt(colorA[1]);
float gBa = (colorA[2] <= 0.25) ? ((16. * colorA[2] - 12.) * colorA[2] + 4.) * colorA[2] : sqrt(colorA[2]);
float gAa = (colorA[3] <= 0.25) ? ((16. * colorA[3] - 12.) * colorA[3] + 4.) * colorA[3] : sqrt(colorA[3]);
float R = (colorB[0] <= 0.5) ? colorA[0] - (1. - 2. * colorB[0]) * colorA[0] * (1. - colorA[0]) : colorA[0] + (2. * colorB[0] - 1.) * (gRa - colorA[0]);
float G = (colorB[1] <= 0.5) ? colorA[1] - (1. - 2. * colorB[1]) * colorA[1] * (1. - colorA[1]) : colorA[1] + (2. * colorB[1] - 1.) * (gGa - colorA[1]);
float B = (colorB[2] <= 0.5) ? colorA[2] - (1. - 2. * colorB[2]) * colorA[2] * (1. - colorA[2]) : colorA[2] + (2. * colorB[2] - 1.) * (gBa - colorA[2]);
float A = (colorB[3] <= 0.5) ? colorA[3] - (1. - 2. * colorB[3]) * colorA[3] * (1. - colorA[3]) : colorA[3] + (2. * colorB[3] - 1.) * (gAa - colorA[3]);
gl_FragColor = vec4(R, G, B, A) * brightness;
}
// http://www.deepskycolors.com/archivo/2010/04/21/formulas-for-Photoshop-blending-modes.html
}