12 novembre 2024

Recette : effet de Sticker en SwiftUI

Je faisais une fois de plus joujou avec les shaders et SwiftUI. Tout a dérapé. Ce qui devait être une petite expérimentation dans la version 2 de Storma s'est terminé en package Swift.

Effet de sticker sur une icône en forme d'éclair

Voici Sticker, un petit utilitaire qui vous permettra d’appliquer un effet carte Pokémon sur n’importe quelle vue. C’est open source et dispo dès à présent sur mon GitHub. N’hésitez pas à jouer avec, à me dire ce que vous en pensez et à lâcher votre meilleure star si le travail vous plaît !

Je suis encore loin d’être un expert des shaders. Leur code est en général peu digeste. C’est pour cette raison que je vous conseille de jeter un œil aux éditeurs visuels de Godot ou Unity si vous souhaitez vous lancer. Connecter des blocs entre eux aide à se familiariser avec leur fonctionnement.

Pour ma part, sur des shaders complexes, j’essaye de bien décomposer le problème en plusieurs parties bien distinctes et je m’aide de ChatGPT sur certains points. Attention toutefois, l’ami Chat a parfois du mal avec les concepts visuels. Pour les plus curieux, voici la recette.

1. Faire un dégradé de couleur

Créez une fonction de pseudo aléatoire basée sur la position du pixel.

// A helper function to generate pseudo-random noise based on position
float random(float2 uv) {
    return fract(sin(dot(uv.xy, float2(12.9898, 78.233))) * 43758.5453);
}

Créez le dégradé en utilisant la fonction d’aléatoire tout en ajoutant une pointe de sinus et cosinus.

float2 uv = position; // colorEffect position input
float contrast = 0.9;
float gradientNoise = random(position) * 0.1;
 
// Calculate less saturated color shifts for a metallic effect
half r = half(contrast + 0.25 * sin(uv.x * 10.0 + gradientNoise));
half g = half(contrast + 0.25 * cos(uv.y * 10.0 + gradientNoise));
half b = half(contrast + 0.25 * sin((uv.x + uv.y) * 10.0 - gradientNoise));
 
half4 foilColor = half4(r, g, b, 1.0);

2. Grillade de losanges

Afin de donner plus de réalisme, créez une grille de carrés et appliquez-y une rotation à 45°.

// Checker pattern function to create a diamond (lozenge) effect
float checkerPattern(float2 uv, float scale, float degreesAngle) {
    float radiansAngle = degreesAngle * M_PI_F / 180;
 
    // Scale the UV coordinates
    uv *= scale;
 
    // Rotate the UV coordinates by the specified angle
    float cosAngle = cos(radiansAngle);
    float sinAngle = sin(radiansAngle);
    float2 rotatedUV = float2(
                              cosAngle * uv.x - sinAngle * uv.y,
                              sinAngle * uv.x + cosAngle * uv.y
                              );
 
    // Determine if the current tile is black or white
    return fmod(floor(rotatedUV.x) + floor(rotatedUV.y), 2.0) == 0.0 ? 0.0 : 1.0;
}

3. Grain de sel

Afin de donner de la texture à votre effet, vous pouvez lui ajouter un effet de grain. Pour cela créez une fonction permettant de générer du bruit1.

float noisePattern(float2 uv) {
    float2 i = floor(uv);
    float2 f = fract(uv);
 
    // Four corners in 2D of a tile
    float a = random(i);
    float b = random(i + float2(1.0, 0.0));
    float c = random(i + float2(0.0, 1.0));
    float d = random(i + float2(1.0, 1.0));
 
    // Smooth Interpolation
    float2 u = smoothstep(0.0, 1.0, f);
 
    // Mix 4 corners percentages
    return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

4. Préparez à mixer

Créez une fonction permettant de calculer la luminosité d’une couleur.

// Helper function to calculate brightness
float calculateBrightness(half4 color) {
    return (color.r * 0.299 + color.g * 0.587 + color.b * 0.114);
}

Créez une fonction utilitaire permettant de mélanger deux couleurs en fonction de la luminosité de la couleur de base.

// Function to mix colors with more intensity on lighter colors
half4 lightnessMix(half4 baseColor, half4 overlayColor, float intensity, float baselineFactor) {
    // Calculate brightness of the base color
    float brightness = calculateBrightness(baseColor);
 
    // Adjust mix factor based on brightness, with a minimum baseline for darker colors
    float adjustedMixFactor = max(smoothstep(0.2, 1.0, brightness) * intensity, baselineFactor);
 
    // Perform color mixing
    return mix(baseColor, overlayColor, adjustedMixFactor);
}

Créez une fonction permettant d’augmenter le contraste d’une couleur.

// Function to increase contrast based on a pattern value
half4 increaseContrast(half4 source, float pattern, float intensity) {
    // Calculate the brightness of the source color
    float brightness = calculateBrightness(source);
 
    // Determine the amount of contrast to apply, based on pattern and brightness
    float contrastFactor = mix(1.0, intensity, pattern * brightness);
 
    // Center the source color around 0.5, apply contrast adjustment, then re-center
    half4 contrastedColor = (source - half4(0.5)) * contrastFactor + half4(0.5);
 
    return contrastedColor;
}

Vous êtes prêt pour la tambouille finale : le mélange.

5. Mixez

Mélangez le tout avec précaution en respectant l’ordre suivant :

  1. Mélangez la couleur d’entrée avec votre dégradé de couleur en prenant en compte la luminosité de la couleur en entrée.
  2. Mélangez le résultat de l’étape nº1 avec la grillade de losange en utilisant un mix augmentant le contraste.
  3. Mélangez le résultat de l’étape n°2 avec votre grain de sel en utilisant encore une fois un mix augmentant le contraste.
half4 mixedFoilColor = lightnessMix(color, foilColor, intensity, 0.3);
half4 checkerFoil = increaseContrast(mixedFoilColor, checker, checkerIntensity);
half4 noiseCheckerFoil = increaseContrast(checkerFoil, noise, noiseIntensity);
 
return noiseCheckerFoil;

Ce mélange vous donnera un effet plus prononcé sur les couleurs claires et plus discret sur les couleurs sombres pour encore plus de réalisme.

Comparaison des icônes avec et sans effet Comparaison des icônes avec et sans effet. Remarquez l'effet plus prononcé sur les couleurs claires.

Footnotes

  1. Cette étape ne constitue aucun danger pour les oreilles attentives de vos voisins.