Zufall & RauschenRND-03
Worley Noise
Punkte streuen, Abstände messen — und heraus kommen Zellen, Schuppen und rissiger Boden.
- Laufzeit
- O(Pixel)
- Kernfrage
- Wie weit ist der nächste Punkt?
- Nachbarschaft
- 9 Zellen je Pixel
- Kachelbar
- ja, nahtlos
01Worum es geht
Perlin Noise gibt weiche Hügel. Worley Noise gibt Zellen. Steven Worley hat es 1996 beschrieben, und die Idee passt in einen Satz: Streue Punkte über die Fläche und frage an jedem Pixel, wie weit der nächste davon entfernt ist.
Mehr ist es nicht — und trotzdem fällt daraus alles heraus, was in der Natur zellig aussieht: Bienenwaben, rissiger Lehmboden, Schuppen, Steinmauern, das Flackern von Licht auf einem Schwimmbadboden. Man kann es auch andersherum sehen: Worley Noise ist ein Voronoi-Diagramm, das nie als Diagramm gezeichnet wird, sondern nur als Abstandsfeld ausgelesen.




02So funktioniert es
Die Punkte im Raster
Alle Punkte durchzusuchen wäre teuer. Deshalb wird die Fläche in ein Raster aus Zellen geteilt und in jede Zelle genau ein Punkt geworfen, irgendwo hinein. Für ein Pixel kommen dann nur die eigene Zelle und ihre acht Nachbarn in Frage — neun Abstände statt tausend. Alles, was weiter weg liegt, ist per Konstruktion weiter weg.
Das Werfen selbst ist wichtig: Ohne die zufällige Verschiebung innerhalb der Zelle wäre das Ergebnis ein sichtbares Gitter.
F1, F2 und der eigentliche Trick
Der Abstand zum nächsten Punkt heißt F1. Er ist nahe an einem Punkt klein und dazwischen groß — das ergibt weiche Blasen.
Interessant wird es mit dem zweitnächsten Punkt, F2. Denn genau auf der Grenze zwischen zwei Punkten sind beide Abstände gleich, also ist F2 − F1 dort null. Diese Differenz zeichnet die Zellgrenzen als dünne dunkle Linien: Adern, Risse, Waben. Ein Bild, drei Zeilen mehr Code.
Weitere gebräuchliche Kombinationen:
1 − F1— Schuppen und Blasen, hell in der MitteF1 · F2— weiche Klumpen mit dunklen Fugen- mehrere Oktaven wie bei Perlin — feiner Stein statt großer Kacheln
Nahtlos
Beim Suchen der Nachbarzellen läuft der Index mit Modulo um den Rand herum, während die Position des Punktes außerhalb bleibt. Dadurch passt die linke Kante an die rechte und die obere an die untere: Die Textur lässt sich beliebig kacheln, ohne dass man die Naht sieht. Für Spiele ist das der Unterschied zwischen brauchbar und unbrauchbar.
Welcher Abstand?
Hier steht der gewöhnliche euklidische Abstand. Nimmt man stattdessen die Manhattan-Distanz, werden aus den runden Zellen eckige Kacheln; mit dem Chebyshev-Abstand Quadrate. Dieselben Punkte, dieselben neun Vergleiche — eine andere Welt.
03Ausprobieren
04Implementierung
// Steven Worley hat das 1996 beschrieben, und es ist das Gegenstück zu Perlin Noise.
// Perlin gibt weiche Hügel, Worley gibt Zellen: Punkte über das Bild streuen und an
// jedem Pixel fragen, wie weit der nächste entfernt ist. Nahe an einem Punkt ist die
// Antwort klein, dazwischen groß - und daraus werden Waben, rissiger Lehm,
// Schuppen, Stein, Lichtmuster auf Wasser.
public static class WorleyNoise
{
// cells sagt, wie viele Punkte es je Richtung gibt. order wählt, nach welchem Punkt
// gefragt wird: 1 ist der nächste, 2 der zweitnächste.
// Das Ergebnis läuft von 0 bis 1 und wiederholt sich nahtlos an den Rändern.
public static float[] Generate(int width, int height, int cells, ulong seed, int order = 1)
{
ArgumentOutOfRangeException.ThrowIfLessThan(order, 1);
var points = Scatter(cells, seed);
var cellWidth = (float)width / cells;
var cellHeight = (float)height / cells;
var noise = new float[width * height];
var distances = new float[9];
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var cellX = (int)(x / cellWidth);
var cellY = (int)(y / cellHeight);
var found = 0;
// Nur die neun Zellen um den Pixel herum können den nächsten Punkt enthalten.
// Alles Weitere liegt schon von der Bauart her weiter weg.
for (var offsetY = -1; offsetY <= 1; offsetY++)
{
for (var offsetX = -1; offsetX <= 1; offsetX++)
{
// Das Modulo lässt das Bild sich wiederholen: Ein Punkt, der rechts
// hinausfällt, kommt links wieder herein.
var neighbourX = (cellX + offsetX + cells) % cells;
var neighbourY = (cellY + offsetY + cells) % cells;
var point = (neighbourY * cells + neighbourX) * 2;
var pointX = (cellX + offsetX + points[point]) * cellWidth;
var pointY = (cellY + offsetY + points[point + 1]) * cellHeight;
var dx = x - pointX;
var dy = y - pointY;
distances[found++] = MathF.Sqrt(dx * dx + dy * dy);
}
}
Array.Sort(distances, 0, found);
// Geteilt durch die Größe einer Zelle, damit die Zahlen dieselben bleiben,
// egal wie groß das Bild ist.
noise[y * width + x] = Math.Min(distances[order - 1] / cellWidth, 1f);
}
}
return noise;
}
// Ein Punkt je Zelle, irgendwo darin. Ohne dieses Verwackeln wäre das
// Muster ein schlichtes Gitter.
private static float[] Scatter(int cells, ulong seed)
{
var random = new Pcg(seed);
var points = new float[cells * cells * 2];
for (var i = 0; i < points.Length; i++)
points[i] = (float)random.NextDouble();
return points;
}
}
05Zellen, Adern, Schuppen
const int size = 240;
const int cells = 8;
const ulong seed = 20240921;
// Der Abstand zum nächsten und zum zweitnächsten Punkt. Beide Bilder kommen
// von denselben Punkten, sie stellen nur eine andere Frage.
var nearest = WorleyNoise.Generate(size, size, cells, seed);
var second = WorleyNoise.Generate(size, size, cells, seed, order: 2);
var veins = new float[nearest.Length];
var scales = new float[nearest.Length];
for (var i = 0; i < nearest.Length; i++)
{
// Wo die Differenz der beiden klein ist, liegt der Pixel genau zwischen zwei
// Punkten - das ist die Grenze einer Zelle. Das ist der Trick, der aus
// Blasen Risse, Adern und Waben macht.
veins[i] = Math.Min((second[i] - nearest[i]) * 2.5f, 1f);
// Umgedreht sieht der nächste Abstand aus wie Schuppen oder Blasen.
scales[i] = 1 - nearest[i];
}
Channels.ToImage(nearest, size, size).Save(Path.Combine(folder, "worley-cells.png"));
Channels.ToImage(veins, size, size).Save(Path.Combine(folder, "worley-veins.png"));
Channels.ToImage(scales, size, size).Save(Path.Combine(folder, "worley-scales.png"));
Console.WriteLine($"{cells * cells} points on {size} x {size} pixels");
Console.WriteLine($"nearest point: {nearest.Min():0.00} to {nearest.Max():0.00}, average {nearest.Average():0.00}");
Console.WriteLine($"second nearest: {second.Min():0.00} to {second.Max():0.00}, average {second.Average():0.00}");
64 points on 240 x 240 pixels
nearest point: 0.00 to 0.98, average 0.43
second nearest: 0.16 to 1.00, average 0.70
06Marmor aus drei Rauschen
const int size = 240;
// Marmor sind Streifen, die verbogen wurden. Die Streifen kommen von einem
// Sinus, das Verbiegen von Perlin Noise und die Maserung des Steins von
// Worley - drei Rauschen, ein Material.
var random = new Pcg(4711);
var seeds = new float[size * size];
for (var i = 0; i < seeds.Length; i++)
seeds[i] = (float)random.NextDouble();
var soft = PerlinNoise.Generate(seeds, size, size, octaves: 5, persistence: 0.55f);
var nearest = WorleyNoise.Generate(size, size, cells: 5, seed: 4711);
var second = WorleyNoise.Generate(size, size, cells: 5, seed: 4711, order: 2);
var marble = new float[seeds.Length];
for (var y = 0; y < size; y++)
{
for (var x = 0; x < size; x++)
{
var i = y * size + x;
// Sechs Streifen über das Bild, vom Rauschen seitlich verschoben.
var stripes = MathF.Sin((x / (float)size * 5 + soft[i] * 4) * MathF.PI);
// Genau zwischen zwei Punkten sind die beiden Abstände fast gleich. Nur dort
// wird das hier klein, und das zeichnet die dünnen dunklen Risse.
var crack = Math.Min((second[i] - nearest[i]) * 14f, 1f);
marble[i] = Math.Clamp((0.66f + stripes * 0.26f) * (0.45f + crack * 0.55f), 0f, 1f);
}
}
Channels.ToImage(marble, size, size).Save(Path.Combine(folder, "worley-marble.png"));
Console.WriteLine($"marble: {marble.Min():0.00} to {marble.Max():0.00}, average {marble.Average():0.00}");
marble: 0.18 to 0.92, average 0.64
07Gut zu wissen
- Der Aufwand steckt in den neun Abständen je Pixel. Auf der Grafikkarte ist genau das der Grund, warum Worley Noise dort in einem Shader läuft: Jedes Pixel rechnet für sich, ohne irgendetwas zu wissen.
- Wurzel sparen. Für den Vergleich reicht das Quadrat des Abstands; die Wurzel braucht man erst ganz am Ende. Hier steht sie trotzdem in der Schleife, weil der Code sonst schwerer zu lesen wäre.
- Die zweite Beispielseite mischt drei Rauschen zu Marmor: Sinusstreifen für die Maserung, Perlin zum Verbiegen, Worley für die Adern. So arbeiten Textur-Werkzeuge tatsächlich — keins der Rauschen macht allein ein Material.
- Verwandt und einen eigenen Blick wert: Jump Flooding, das ein vollständiges Voronoi-Diagramm in wenigen Durchläufen auf der Grafikkarte erzeugt, und Poisson Disk Sampling, das Punkte mit Mindestabstand streut, wenn das Zellraster zu regelmäßig wirkt.