Noise — smoothstep, fade, gradients, FBM
Le bruit procédural (ou noise) est la base de tout contenu généré — terrain, nuages, textures, animations organiques. Ce chapitre couvre des fonctions utilitaires, du plus simple au plus complexe.
smoothstep — la courbe en S
smoothstep est intégrée dans MSL. Elle produit une interpolation douce entre 0 et 1 avec des dérivées nulles aux extrémités — pas de saut brutal.
// MSL natif :
float s = smoothstep(edge0, edge1, x);
// → 0 si x ≤ edge0, 1 si x ≥ edge1, courbe entre les deux
// Formule interne :
// t = clamp((x - edge0) / (edge1 - edge0), 0, 1)
// return t * t * (3 - 2 * t)
// Visualisation :
// 1 ┤ ╭──────
// │ ╭───╯
// │ ╭───╯
// 0 ┼────╯
// e0 e1
inline float inverse_smoothstep(float x)
{
return 0.5 - sin(asin(1.0 - 2.0 * x) / 3.0);
}
smoothstep vs smootherstep
// smoothstep — Ken Perlin (1ère version)
float smoothstep(float t)
{
return t * t * (3.0 - 2.0 * t);
}
// smootherstep — Ken Perlin (version améliorée, dérivée 2 aussi nulle)
float smootherstep(float t)
{
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Différence : smootherstep a les dérivées 1ère ET 2ème nulles aux extrémités
// → pas d'artefact de gradient visible dans les textures de bruit
fade() — le smootherstep de Perlin
// Dans Helpers.metal :
inline float fade(float t)
{
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Utilisée dans perlinNoise3D pour interpoler sur X, Y, Z :
float3 u = float3(fade(pf.x), fade(pf.y), fade(pf.z));
// Pourquoi pas lerp direct ?
// Sans fade : les gradients de bruit créent des discontinuités visibles
// aux frontières des cellules — le bruit devient "en damier"
// Avec fade : transitions invisibles, texture organique
gradient3D() — vecteurs de gradient
Le bruit de Perlin associe un vecteur gradient à chaque coin de la grille. Le produit scalaire entre ce gradient et la distance au coin donne la contribution de ce coin.
// Dans Helpers.metal — gradient3D(hash) :
inline float3 gradient3D(uint hash)
{
uint h = hash & 15; // 16 gradients possibles
float u = h < 8 ? 1.0 : -1.0;
float v = h < 4 ? 1.0 : (h == 12 || h == 14 ? 1.0 : -1.0);
float w = h < 2 ? 1.0 : (h == 12 || h == 13 ? -1.0 : 1.0);
return normalize(float3(u, v, w));
}
// Ce sont 16 des 12 arêtes d'un cube unité
// → équidistribués sur la sphère unité
// → minimise les artefacts directionnels
// Dans perlinNoise3D :
float3 g000 = gradient3D(aaa);
float n000 = dot(g000, pf - float3(0, 0, 0)); // contribution du coin (0,0,0)
perlinNoise3D() — le pipeline complet
inline float perlinNoise3D(float3 p, constant uint* perm)
{
float3 pi = floor(p); // coin entier
float3 pf = fract(p); // fraction dans la cellule
int3 i = int3(pi) & 255; // index dans la table de permutation
float3 u = float3(
fade(pf.x), fade(pf.y), fade(pf.z) // courbe d'interpolation
);
// 8 coins du cube → hash → gradient
uint aaa = perm[perm[perm[i.x ] + i.y ] + i.z ];
uint baa = perm[perm[perm[i.x+1] + i.y ] + i.z ];
// ... (6 autres)
// Produit scalaire gradient · distance pour les 8 coins
float n000 = dot(gradient3D(aaa), pf);
// ... (7 autres)
// Interpolation trilinéaire avec fade
float nx00 = mix(n000, n100, u.x);
float nxy0 = mix(nx00, nx10, u.y);
return mix(nxy0, nxy1, u.z); // résultat ∈ [-1, 1]
}
perm est une table de permutation de 256 entiers (0–255) mélangés une fois côté CPU. Elle garantit que le hash est déterministe et reproductible. À passer via constant uint* perm [[buffer(n)]] depuis le CPU.
Simplex Noise 2D — plus rapide que Perlin
Le simplex noise utilise une grille triangulaire (simplex) plutôt que cubique. En 2D : 3 coins au lieu de 4. En 3D : 4 coins au lieu de 8. Moins d'artefacts directionnels, plus rapide.
inline float simplexNoise2D(float2 p, uint seed)
{
// F2 et G2 : facteurs de skew pour passer grille carrée ↔ grille simplex
const float F2 = 0.5 * (sqrt(3.0) - 1.0); // ≈ 0.366
const float G2 = (3.0 - sqrt(3.0)) / 6.0; // ≈ 0.211
// 1. Skew — trouver la cellule simplex
float s = (p.x + p.y) * F2;
float2 pi = floor(p + s);
// 2. Unskew — position dans la cellule
float t = (pi.x + pi.y) * G2;
float2 p0 = p - (pi - t);
// 3. Déterminer quel triangle (p0.x > p0.y → triangle bas, sinon haut)
int2 i1 = p0.x > p0.y ? int2(1, 0) : int2(0, 1);
// 4. Contributions des 3 coins (atténuation radiale)
float t0 = 0.5 - dot(p0, p0);
if (t0 > 0.0) { t0 *= t0; n0 = t0 * t0 * dot(g0, p0); }
return 70.0 * (n0 + n1 + n2); // ∈ [-1, 1]
}
FBM — Fractal Brownian Motion
Le FBM superpose plusieurs octaves de bruit à fréquences et amplitudes croissantes — chaque octave ajoute du détail fin. C'est la base du terrain procédural, des nuages, des textures organiques.
inline float fbm2D(float2 p, int octaves, float persistence,
float lacunarity, uint seed)
{
float value = 0.0;
float amplitude = 1.0; // réduit à chaque octave (persistence)
float frequency = 1.0; // double à chaque octave (lacunarity)
float maxValue = 0.0; // pour normaliser en fin de boucle
for (int i = 0; i < octaves; i++) {
value += amplitude * simplexNoise2D(p * frequency, seed + i * 1337);
maxValue += amplitude;
amplitude *= persistence; // typiquement 0.5 → chaque octave = moitié moins fort
frequency *= lacunarity; // typiquement 2.0 → chaque octave = 2× plus fin
}
return value / maxValue; // normalise dans [-1, 1]
}
// Usage terrain :
float height = fbm2D(worldPos.xz * 0.01, 6, 0.5, 2.0, seed);
// 6 octaves, persistence 0.5, lacunarity 2.0 (standard)
| Paramètre | Valeur typique | Effet si augmenté |
|---|---|---|
octaves |
4–8 | Plus de détail fin — coût GPU linéaire |
persistence |
0.5 | Surface plus rugueuse, chaotique |
lacunarity |
2.0 | Détails plus fins entre octaves |
Domain Warping — tordre l'espace
Le domain warping (Inigo Quilez) utilise du FBM pour déformer les coordonnées d'entrée d'un autre FBM. Résultat : formes organiques impossibles à obtenir autrement — rivières, formations rocheuses, nuages tourbillonnants.
// Dans ton Helpers.metal — warpedNoise() :
inline float warpedNoise(float2 p, uint seed)
{
// Passe 1 : q = FBM(p) — première déformation
float2 q = float2(
fbm2D(p, 4, 0.5, 2.0, seed),
fbm2D(p + float2(5.2, 1.3), 4, 0.5, 2.0, seed)
);
// Passe 2 : r = FBM(p + q) — deuxième déformation, utilise q comme offset
float2 r = float2(
fbm2D(p + 4.0 * q + float2(1.7, 9.2), 4, 0.5, 2.0, seed),
fbm2D(p + 4.0 * q + float2(8.3, 2.8), 4, 0.5, 2.0, seed)
);
// Résultat : FBM(p + r) — troisième passe, déformée deux fois
return fbm2D(p + 4.0 * r, 4, 0.5, 2.0, seed);
}
// Coût : 10× fbm2D avec 4 octaves = 40 appels simplex par pixel
// À utiliser sur texture basse résolution puis upscale, pas par vertex en temps réel
Voronoi — cellules organiques
Le bruit de Voronoi associe chaque point à son "centre de cellule" le plus proche. Utilisé pour les biomes, les textures de pierre, les veines.
inline VoronoiResult voronoi2D(float2 p, uint seed)
{
float2 pi = floor(p);
float2 pf = fract(p);
VoronoiResult result;
result.distance1 = 100.0; // distance au centre le plus proche
result.distance2 = 100.0; // distance au 2ème centre (pour les bords)
// Tester les 9 cellules voisines
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
uint h = hash2(uint2(pi + float2(x,y)) + seed);
// Point aléatoire à l'intérieur de la cellule voisine
float2 point = float2(x,y) + float2(hashToFloat(h), hashToFloat(hashPerlin(h)));
float dist = length(pf - point);
// Garder les deux plus proches
if (dist < result.distance1) { result.distance2 = result.distance1; result.distance1 = dist; }
else if (dist < result.distance2) { result.distance2 = dist; }
}
}
return result;
}
// Usages :
float cell = result.distance1; // remplissage de cellule
float edge = result.distance2 - result.distance1; // bords entre cellules
uint id = result.cellId; // couleur unique par cellule (biome)