Schreiben von echtem Perlin-Rauschen

Perlins Geräusch stößt bei einer Suchanfrage sofort auf diese Übersetzung auf Habré. Wie in den Kommentaren zur Veröffentlichung zu Recht angemerkt, geht es hier überhaupt nicht um Perlins Lärm. Vielleicht wusste der Autor der Übersetzung selbst nicht Bescheid.

Was Perlins Rauschen positiv auszeichnet, merkt man leicht , wenn man die Bilder vergleicht.

Normales Rauschen (aus demselben Artikel):
Bild

Perlin-Rauschen:
Bild

Und durch Erhöhen der Oktavenzahl kann das erste Bild nicht näher an das zweite gebracht werden. Ich werde die Vorteile von Perlin-Rauschen und seinen Umfang nicht beschreiben (da es sich um Programmierung handelt, nicht um Anwendung), aber ich werde versuchen zu erklären, wie es implementiert wird. Ich denke, dies wird für viele Programmierer nützlich sein, da Ken Perlins Hacker-Quellen nicht viel erklären, selbst wenn es Kommentare gibt.


Rückzug


Überraschenderweise stellte sich nach den Bewertungen in PM und im Kommentar heraus, dass nicht jeder in der Lage ist, einen Unterschied zwischen einem einfachen, weichen Graustufenrauschen und Perlins Rauschen zu erkennen. Aber wahrscheinlich erschien der Artikel gerade wegen dieses Paradoxons und war populär.

Ich werde versuchen, einen Tipp zu geben: Das
erste Bild besteht aus ausgeprägten Pixeln (vergrößert) in verschiedenen Graustufen:

Das zweite (Perlin-Rauschen) sieht aus wie schwarz-weiße, verschwommene Würmer.

Folgendes passiert nach einfachen Operationen in einem Grafikeditor (Suche nach Rahmen, Invertieren, Posterisieren):
Perlin:

Bild aus dem Artikel (genau dieselben Operationen werden angewendet):


Ja, bei fraktalen Geräuschen ist es bei vielen Oktaven wirklich schwierig zu verstehen, was im Original enthalten ist - Perlin oder nicht. Dies ist jedoch kein Grund, das fraktale Rauschen als Perlin-Rauschen zu bezeichnen.
Dies wird mit einer Beschreibung des Unterschieds enden.
Das Ende des Rückzugs.

Betrachten wir eine zweidimensionale Option. Im Moment werden wir nur die leere Klasse schreiben. Die Eingabe ist ein zweidimensionaler Vektor oder zwei Gleitkommazahlen: x, y.

Der Rückgabewert ist eine Zahl von -1,0 bis 1,0:
public class Perlin2d
{
  public float Noise(float x, float y)
  {
    throw new NotImplementedException();
  }
}

Ein paar Worte zur Interpolation


Die Idee des normalen geglätteten Rauschens ist, dass es ein diskretes Gitter von Pseudozufallswerten gibt und eine Interpolation zwischen den Gitterknoten für den angeforderten Punkt stattfindet (je näher der Punkt an einem Gitterknoten liegt, desto mehr entspricht sein Wert dem Knotenwert).

Hier, im dritten bedingten Quadrat, hat der Punkt in der Mitte nach der Interpolation den Wert 3:

Bild

Betrachten wir genauer, wie sich das Tripel dort herausstellt. Punktkoordinaten: Ganzzahlige Koordinaten des Punktes (obere linke Ecke des Quadrats): Ermittelt durch Abrunden auf die untere Seite (Bodenfunktion). Die lokalen Koordinaten des Punktes innerhalb des Quadrats werden durch Subtraktion erhalten:
x:2.5,
y:0.5



x:2,
y:0




x = 2.5 – 2 = 0.5,
y = 0.5 – 0 = 0.5


Wir nehmen den Wert der oberen linken Ecke des Quadrats (1) und der oberen rechten Ecke (2). Interpolieren Sie die Oberseite mit der lokalen Koordinate x (0,5). Die lineare Interpolation sieht folgendermaßen aus:
static float Lerp(float a, float b, float t)
{
// return a * (t - 1) + b * t; можно переписать с одним умножением (раскрыть скобки, взять в другие скобки):
  return a + (b - a) * t;
}


Wir nehmen den Wert der unteren linken Ecke des Quadrats (2) und der unteren rechten Ecke (7). Interpolieren Sie die Unterseite mit derselben lokalen Koordinate x (0,5).
Ergebnisse: Es bleibt nun die Interpolation von Ober- und Unterkante mit der lokalen y-Koordinate (ebenfalls 0,5): Bilineare Interpolation ist die einfachste, aber das Ergebnis ist nicht die attraktivste. Andere Interpolationsoptionen umfassen das Ändern der lokalen Koordinate (Parameter t) vor der Interpolation. Gleichmäßigere Übergänge werden in der Nähe der Grenzwerte (0 und 1) erhalten. Bei dem Rauschen von Perlin handelt es sich bei der ersten Option um eine ziemlich starke Krümmung.
верхняя: 1.5
нижняя: 4.5



1.5 * 0.5 + 4.5 * (1 – 0.5) = 3






Bild


static float QunticCurve(float t)
{
  return t * t * t * (t * (t * 6 - 15) + 10);
}
...
// комбинирование с функцией линейной интерполяции:
Lerp(a, b, QuinticCurve(t))


Die Hauptidee und der Unterschied von Perlin-Rauschen


Es ist sehr einfach:
1. Die Knoten des Gitters - Pseudo-Zufallsvektor (zweidimensionaler zweidimensionale Rauschen, dreidimensional dreidimensional, und so weiter), eher als Pseudo-Zufallszahl .
2. Interpolieren Sie zwischen den Skalarprodukten von a) Vektoren von den Eckpunkten des Quadrats zu einem Punkt innerhalb des Quadrats (ein Würfel in der dreidimensionalen Version) und b) Pseudozufallsvektoren (bei der Beschreibung des Perlin-Rauschens werden sie Gradientenvektoren genannt).

In seiner verbesserten Version von Rauschen verwendet Ken Perlin nur 12 Gradientenvektoren. Für die zweidimensionale Version sind nur 4 erforderlich - je nach Anzahl der Flächen (das Quadrat hat 4). Die Vektoren sind (bedingt von der Mitte des Würfels / Quadrats) auf jede der Flächen gerichtet und nicht normalisiert.

Hier sind sie:
{  1, 0 }
{ -1, 0 }
{  0, 1 }
{  0,-1 }


Bild

Jeder Knoten des Gitters entspricht also einem von vier Vektoren. Der Vektor sei ein Array von Floats.
    float[] GetPseudoRandomGradientVector(int x, int y)
    {
        int v = // псевдо-случайное число от 0 до 3 которое всегда неизменно при данных x и y
        switch (v)
        {
            case 0:  return new float[]{  1, 0 };
            case 1:  return new float[]{ -1, 0 };
            case 2:  return new float[]{  0, 1 };
            default: return new float[]{  0,-1 };
        }
    }


Implementierung


Wir benötigen ein Skalarprodukt von Vektoren:
    static float Dot(float[] a, float[] b)
    {
        return a[0] * b[0] + a[1] * b[1];
    }


Die Hauptmethode:
    public float Noise(float fx, float fy)
    {
        // сразу находим координаты левой верхней вершины квадрата
        int left = (int)System.Math.Floor(fx);
        int top  = (int)System.Math.Floor(fy);
        // а теперь локальные координаты точки внутри квадрата
        float pointInQuadX = fx - left;
        float pointInQuadY = fy - top;
        // извлекаем градиентные векторы для всех вершин квадрата:
        float[] topLeftGradient     = GetPseudoRandomGradientVector(left,   top  );
        float[] topRightGradient    = GetPseudoRandomGradientVector(left+1, top  );
        float[] bottomLeftGradient  = GetPseudoRandomGradientVector(left,   top+1);
        float[] bottomRightGradient = GetPseudoRandomGradientVector(left+1, top+1);
        // вектора от вершин квадрата до точки внутри квадрата:
        float[] distanceToTopLeft     = new float[]{ pointInQuadX,   pointInQuadY   };
        float[] distanceToTopRight    = new float[]{ pointInQuadX-1, pointInQuadY   };
        float[] distanceToBottomLeft  = new float[]{ pointInQuadX,   pointInQuadY-1 };
        float[] distanceToBottomRight = new float[]{ pointInQuadX-1, pointInQuadY-1 };
        // считаем скалярные произведения между которыми будем интерполировать
/*
 tx1--tx2
  |    |
 bx1--bx2
*/
        float tx1 = Dot(distanceToTopLeft,     topLeftGradient);
        float tx2 = Dot(distanceToTopRight,    topRightGradient);
        float bx1 = Dot(distanceToBottomLeft,  bottomLeftGradient);
        float bx2 = Dot(distanceToBottomRight, bottomRightGradient);
        // готовим параметры интерполяции, чтобы она не была линейной:
        pointInQuadX = QunticCurve(pointInQuadX);
        pointInQuadY = QunticCurve(pointInQuadY);
        // собственно, интерполяция:
        float tx = Lerp(tx1, tx2, pointInQuadX);
        float bx = Lerp(bx1, bx2, pointInQuadX);
        float tb = Lerp(tx, bx, pointInQuadY);
        // возвращаем результат:
        return tb;
    }


Als Bonus:
Multi-Oktaven-Rauschen
    public float Noise(float fx, float fy, int octaves, float persistence = 0.5f)
    {
        float amplitude = 1; // сила применения шума к общей картине, будет уменьшаться с "мельчанием" шума
        // как сильно уменьшаться - регулирует persistence
        float max = 0; // необходимо для нормализации результата
        float result = 0; // накопитель результата
        while (octaves-- > 0)
        {
            max += amplitude;
            result += Noise(fx, fy) * amplitude;
            amplitude *= persistence;
            fx *= 2; // удваиваем частоту шума (делаем его более мелким) с каждой октавой
            fy *= 2;
        }
        return result/max;
    }



Und der letzte benutzt eine Tabelle mit Zufallszahlen. In Ken Perlins Code wird eine solche Tabelle manuell geschrieben, und die Werte werden von dort auf eine völlig andere Weise abgerufen. Hier kann man experimentieren und die Rauschgleichmäßigkeit und das Fehlen von expliziten Mustern hängen stark davon ab.

Ich habe gemacht
so
class Perlin2D
{
    byte[] permutationTable;
    public Perlin2D(int seed = 0)
    {
        var rand = new System.Random(seed);
        permutationTable = new byte[1024];
        rand.NextBytes(permutationTable); // заполняем случайными байтами
    }
    private float[] GetPseudoRandomGradientVector(int x, int y)
    {
// хэш-функция с Простыми числами, обрезкой результата до размера массива со случайными байтами
        int v = (int)(((x * 1836311903) ^ (y * 2971215073) + 4807526976) & 1023);
        v = permutationTable[v]&3;
        switch (v)
        {
            ...


& 3 schneidet alle int32-Zahlen auf 3 ab. Lesen Sie mehr über die AND-Operation in Wikipedia. Eine
Operation wie % 3 würde ebenfalls funktionieren, jedoch viel langsamer.


Gesamter Quellcode (kein Kommentar)
class Perlin2D
{
    byte[] permutationTable;
    public Perlin2D(int seed = 0)
    {
        var rand = new System.Random(seed);
        permutationTable = new byte[1024];
        rand.NextBytes(permutationTable);
    }
    private float[] GetPseudoRandomGradientVector(int x, int y)
    {
        int v = (int)(((x * 1836311903) ^ (y * 2971215073) + 4807526976) & 1023);
        v = permutationTable[v]&3;
        switch (v)
        {
            case 0:  return new float[]{  1, 0 };
            case 1:  return new float[]{ -1, 0 };
            case 2:  return new float[]{  0, 1 };
            default: return new float[]{  0,-1 };
        }
    }
    static float QunticCurve(float t)
    {
        return t * t * t * (t * (t * 6 - 15) + 10);
    }
    static float Lerp(float a, float b, float t)
    {
        return a + (b - a) * t;
    }
    static float Dot(float[] a, float[] b)
    {
        return a[0] * b[0] + a[1] * b[1];
    }
    public float Noise(float fx, float fy)
    {
        int left = (int)System.Math.Floor(fx);
        int top  = (int)System.Math.Floor(fy);
        float pointInQuadX = fx - left;
        float pointInQuadY = fy - top;
        float[] topLeftGradient     = GetPseudoRandomGradientVector(left,   top  );
        float[] topRightGradient    = GetPseudoRandomGradientVector(left+1, top  );
        float[] bottomLeftGradient  = GetPseudoRandomGradientVector(left,   top+1);
        float[] bottomRightGradient = GetPseudoRandomGradientVector(left+1, top+1);
        float[] distanceToTopLeft     = new float[]{ pointInQuadX,   pointInQuadY   };
        float[] distanceToTopRight    = new float[]{ pointInQuadX-1, pointInQuadY   };
        float[] distanceToBottomLeft  = new float[]{ pointInQuadX,   pointInQuadY-1 };
        float[] distanceToBottomRight = new float[]{ pointInQuadX-1, pointInQuadY-1 };
        float tx1 = Dot(distanceToTopLeft,     topLeftGradient);
        float tx2 = Dot(distanceToTopRight,    topRightGradient);
        float bx1 = Dot(distanceToBottomLeft,  bottomLeftGradient);
        float bx2 = Dot(distanceToBottomRight, bottomRightGradient);
        pointInQuadX = QunticCurve(pointInQuadX);
        pointInQuadY = QunticCurve(pointInQuadY);
        float tx = Lerp(tx1, tx2, pointInQuadX);
        float bx = Lerp(bx1, bx2, pointInQuadX);
        float tb = Lerp(tx, bx, pointInQuadY);
        return tb;
    }
    public float Noise(float fx, float fy, int octaves, float persistence = 0.5f)
    {
        float amplitude = 1;
        float max = 0;
        float result = 0;
        while (octaves-- > 0)
        {
            max += amplitude;
            result += Noise(fx, fy) * amplitude;
            amplitude *= persistence;
            fx *= 2;
            fy *= 2;
        }
        return result/max;
    }
}



Ergebnis:
Bild

Jetzt auch beliebt: