Création d'effets visuels 2D avec les shaders de fragments OpenGL ES 3.0 sur Android

Traitement d'image parallèle avec les shaders de fragments

Dans le pipeline graphique d'OpenGL ES, le shader de fragments (Fragment Shader) est l'étape idéale pour appliquer des effets visuels et des filtres post-traitement. Contrairement à une approche séquentielle sur CPU qui itère sur chaque pixel via des boucles imbriquées, le GPU exécute le shader de fragments en parallèle pour chaque pixel (fragment) généré lors de la rasterisation. Cette architecture permet d'obtenir des performances optimales pour les opérations de traitement d'image en temps réel sur Android.

Pour manipuler une texture d'entrée, le shader lit les coordonnées de texture interpolées, échantillonne la couleur via la fonction texture(), applique des transformations mathématiques, et écrit le résultat final dans la variable de sortie. L'utilisation de fonctions mathématiques intégrées à GLSL (comme step, mix, fract, et smoothstep) est privilégiée par rapport aux structures conditionnelles (if/else) pour éviter les divergences de threads et maximiser le débit du GPU.

Architecture de base du shader

Voici la structure fondamentale utilisée pour tous les effets présentés. Elle repose sur le profil GLSL ES 3.0, avec des variables uniformes pour les paramètres dynamiques et la texture source.

#version 300 es
precision mediump float;

uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_progress; // Paramètre d'animation normalisé [0.0, 1.0]

in vec2 v_uv;
out vec4 out_color;

void main() {
    vec4 texColor = texture(u_texture, v_uv);
    out_color = texColor;
}

Implémentation des effets de transition et filtres

1. Grille animée (Dynamic Grid)

Cet effet divise l'image en une grille de cellules carrées. Le paramètre u_progress contrôle la taille de la bordure blanche de chaque cellule, créant un effet de rétrécissement ou d'expansion des tuiles. L'implémentation utilise la fonction fract pour obtenir les coordonnées locales de chaque cellule et step pour générer un masque binaire sans branchements conditionnels.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_progress;
in vec2 v_uv;
out vec4 out_color;

void main() {
    vec2 pixelCoord = v_uv * u_resolution;
    float cellSize = u_resolution.y / 8.0;
    vec2 cellUv = fract(pixelCoord / cellSize);
    
    float margin = u_progress * 0.25;
    float mask = step(margin, cellUv.x) * step(margin, cellUv.y) * 
                 step(cellUv.x, 1.0 - margin) * step(cellUv.y, 1.0 - margin);
                 
    vec4 texColor = texture(u_texture, v_uv);
    out_color = mix(vec4(1.0), texColor, mask);
}

2. Cercle d'expansion (Scaling Circle)

Pour créer une transition circulaire, nous utilisons le concept de champ de distance signée (SDF). La distance entre le fragment actuel et le centre de l'écran est comparée à un rayon dynamique. La correction du ratio d'aspect est essentielle pour éviter que le cercle ne paraisse étiré sur des écrans aux proportions non carrées.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_progress;
in vec2 v_uv;
out vec4 out_color;

void main() {
    vec2 center = vec2(0.5);
    float aspect = u_resolution.x / u_resolution.y;
    vec2 uvAspect = (v_uv - center) * vec2(aspect, 1.0) + center;
    vec2 centerAspect = center * vec2(aspect, 1.0);
    
    float dist = distance(uvAspect, centerAspect);
    float radius = mix(1.5, 0.0, u_progress);
    
    float mask = step(dist, radius);
    out_color = mix(vec4(1.0), texture(u_texture, v_uv), mask);
}

3. Pavage de texture (Tiling)

L'effet de pavage (ou répétition) est obtenu en multipliant les coordonnées de texture par un facteur d'échelle et en extrayant la partie fractionnaire. Cela force les coordonnées à boucler dans l'intervalle [0.0, 1.0], répétant ainsi la texture de manière périodique sur les axes X et Y.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform float u_tiles;
in vec2 v_uv;
out vec4 out_color;

void main() {
    vec2 tiledUv = fract(v_uv * u_tiles);
    out_color = texture(u_texture, tiledUv);
}

4. Stores horizontaux (Horizontal Blinds)

Cet effet simule l'ouverture ou la fermeture de stores vénitiens. En aplpiquant la fonction fract sur la coordonnée Y multipliée par le nombre de lamelles, nous isolons la position verticale à l'intérieur de chaque lamelle. Le masque est ensuite calculé en comparant cette position à la hauteur actuelle de la lamelle.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform float u_progress;
uniform float u_blindCount;
in vec2 v_uv;
out vec4 out_color;

void main() {
    float blindUv = fract(v_uv.y * u_blindCount);
    float blindHeight = mix(1.0, 0.0, u_progress);
    
    float mask = step(blindHeight, blindUv);
    out_color = mix(vec4(1.0), texture(u_texture, v_uv), mask);
}

5. Transition par dissolution (Dissolve)

La dissolution repose sur un bruit pseudo-aléatoire généré directement dans le shader. Une fonction de hachage transforme les coordonnées spatiales en une valeur de bruit normalisée. Les fragments dont la valeur de bruit est inférieure à la progression actuelle sont rendus opaques, tandis que les autres deviennent transparents (ou blancs).

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform float u_progress;
in vec2 v_uv;
out vec4 out_color;

float hash(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

void main() {
    float noise = hash(v_uv);
    float mask = step(noise, u_progress);
    out_color = mix(vec4(1.0), texture(u_texture, v_uv), mask);
}

6. Balayage central (Center Wipe)

Pour un effet de révélation symétrique partant des bords vers le centre, nous calculons la distance absolue de chaque fragment par rapport à l'axe médian vertical. Un seuil dynamique détermine la zone de visibilité.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform float u_progress;
in vec2 v_uv;
out vec4 out_color;

void main() {
    float distFromCenter = abs(v_uv.x - 0.5);
    float threshold = mix(0.5, 0.0, u_progress);
    
    float mask = step(threshold, distFromCenter);
    out_color = mix(vec4(1.0), texture(u_texture, v_uv), mask);
}

7. Balayage radial (Radial Wipe)

Le balayage radial utilise la fonction atan pour déterminer l'angle polaire de chaque fragment par rapport au centre de l'image. L'angle, initialement compris entre -π et π, est normalisé dans l'intervalle [0.0, 1.0] pour être comparé directement avec le paramètre de progressoin.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform float u_progress;
in vec2 v_uv;
out vec4 out_color;

void main() {
    vec2 center = vec2(0.5);
    vec2 dir = v_uv - center;
    
    float angle = atan(dir.y, dir.x);
    float normalizedAngle = (angle + 3.14159265) / (2.0 * 3.14159265);
    
    float mask = step(normalizedAngle, u_progress);
    out_color = mix(vec4(1.0), texture(u_texture, v_uv), mask);
}

8. Pixelisation (Mosaic)

L'effet de pixelisation ou de mosaïque est obtenu en quantifiant les coordonnées de texture continues en une grille discrète. En multipliant les coordonnées par la résolution de la grille, en tronquant avec floor, puis en divisant par la même résolution, nous forçons tous les fragments d'une même cellule à partager exactement la même coordonnée d'échantillonnage.

#version 300 es
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_pixelSize;
in vec2 v_uv;
out vec4 out_color;

void main() {
    vec2 pixelGrid = u_resolution / u_pixelSize;
    vec2 pixelatedUv = floor(v_uv * pixelGrid) / pixelGrid;
    
    out_color = texture(u_texture, pixelatedUv);
}

Étiquettes: OpenGL ES 3.0 GLSL Android Fragment Shader Traitement d'image

Publié le 23 juillet à 16h51