senäh

17senäh und so…

Noizy Thumb

HTML/CSS/JS
23. Apr 2012
Kommentare: 2

Value Noise

Kategorien: HTML/CSS/JS | 23. Apr 2012 | Kommentare: 2

Serie: Noise Functions

Hurra! Die erste Implementierung einer Noise Function steht an. Als erstes knüpfen wir uns Value Noise vor. Das liegt zum einem daran, dass die Theorie hinter Value Noise sehr schnell erklärt ist, und zum anderen daran, dass im Internet leider ein paar falsche Informationen über Value Noise kursieren. Diese möchte ich als erstes aus dem Weg räumen :)

Value Noise – NICHT Perlin Noise!

Eins muss ich hier noch einmal klarstellen: Value Noise ist nicht Perlin Noise! Für diesen Artikel beziehe ich mich hauptsächlich auf ein Beispiel von Hugo Elias, in welchem er erklärt, was Value Noise ist. Dummerweise hat er seinem Artikel aber die Überschrift Perlin Noise verpasst. Das ist falsch! Aber da sein Artikel im Netz stark verbreitet ist, hat sich der Aberglauben verbreitet, dass Perlin Noise und Value Noise das Gleiche wären. Eh eh… falsch!

Lasst euch also nicht in die Irre führen ;) Perlin Noise folgt ein anderes Mal. Heute geht es um Value Noise.

Value Noise in zwei Stufen

Der Algorithmus zur Erzeugung von Value Nosie läuft in zwei Stufen ab. Zuerst generiert man pseudo-zufällige Werte und anschließend interpoliert man diese. Das spätere Erscheinungsbild des Rauschens hängt deswegen auch hauptsächlich von der Art ab, wie man diese Werte generiert und wie man eigentlich interpoliert. Für ein 1dimensionales Rauschen könnte das Ergebnis so aussehen:

1. Phase: Werte generieren

1. Phase: Werte generieren

2. Phase: Werte interpolieren

2. Phase: Werte interpolieren

Um das Erscheinungsbild weiter zu manipulieren, werden wir mehrere Rauschergebnisse übereinander lagern. Dadurch können wir durch die weiteren Parameter Frequenz, Amplitude, Persistenz und Oktave ein feineres und ausgeprägteres Rauschen erzeugen.

rechts: Frequenz * 2

rechts: Frequenz * 2

rechts: Amplitude * 2

rechts: Amplitude * 2

rechts: Oktave * 2

rechts: Oktave * 2

rechts: Oktave * 2, Persistenz * 2

rechts: Oktave * 2, Persistenz * 2

Implementierung in JavaScript

Um Value Noise zu implementieren habe ich außerdem noch auf einen zweiten Artikel zurückgegriffen. Allain hat im XNA.mag Forum beschrieben, wie man Perlin Noise in C#/XNA implementiert. Im Prinzip habe ich seinen Code nur nach JavaScript portiert.

Heute möchte ich für meie Bibliothek Noizy nur eine erste Testvariante von Value Noise einbauen – noch keine Optimierung, noch keine finale API. Aber der Algorithmus soll funktionieren und brauchbare Ergebnisse liefern. Fange wir mit einer Dimension an. Los geht’s!

 1D: Pseudo-zufällige Werte

Was heißt eigentlich pseudo-zufällig? Pseudo-zufällig heißt u.a., dass unsere generierten Werte nicht wirklich “zufällig” im Sinne des Wortes sind und zum anderen, dass wir – mit bekannten Eingangsgrößen – sogar haargenau die gleichen Werte immer und immer wieder erhalten. Also alles andere als zufällige Ergebnisse. ABER unsere Ergebnisse sehen zufällig aus. Elias verwendet für diesen Zweck folgende Funktion:

var getNoise1D = function(x) {
    x = (x << 13) ^ x;
    return (1.0 - ((x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);
};

Er nennt diese Funktionen einen Pseudo Random Number Generator (PRNG). Das stimmt soooo nicht ganz an. (Wieder einmal bei ihm.) Ein PRNG erhält eine Eingabe (= seed, siehe ersten Artikel dieser Serie) und erzeugt damit eine Reihe(!) pseudo-zufälliger Werte. Im Grunde ist diese Funktion aber eine Hash Funktion! Eine Hash Funktion erhält eine Eingabe und weist dieser eine Ausgabe zu. (Nichtsdestotrotz bilden Hash Funktionen häufig die Basis für PRNG’s. Sie sind also eng miteinander verzahnt.) Der Wertebereich der Ausgabewerte dieser Hash Funktion liegt übrigens zwischen -1 und 1. Leider, leider funktioniert die Hash Funktion so nicht zufriedenstellend in JavaScript. Bei höheren Eingabewerten erhält man keine pseudo-zufälligen Ausgabewerte mehr. Das liegt an ihrer Funktionsweise, welche auf bitwise operations mit 32bit Integern basiert. In JavaScript sind aber alle Zahlen 64bit floats. Wir benötigen deswegen eine andere Hash Funktion. Ich habe mich für Bob Jenkins Hash Funktion entschieden, welche für Testzwecke u.a. in Googles V8 Engine verwendet wird:

var getNoise1D = function(x) {
    x = ((x + 0x7ed55d16) + (x << 12))  & 0xffffffff;
    x = ((x ^ 0xc761c23c) ^ (x >>> 19)) & 0xffffffff;
    x = ((x + 0x165667b1) + (x << 5))   & 0xffffffff;
    x = ((x + 0xd3a2646c) ^ (x << 9))   & 0xffffffff;
    x = ((x + 0xfd7046c5) + (x << 3))   & 0xffffffff;
    x = ((x ^ 0xb55a4f09) ^ (x >>> 16)) & 0xffffffff;
    return (x & 0xfffffff) / 0x10000000;
};

Bob Jenkins Hash Funktion hasht die Eingabewert übrigens auf einen Ausgabewertebereich von 0 bis 1. Wenn ich ehrlich bin… bitwise operations größtenteils Voodoo für mich ;) Nur so viel zu Erklärung: Die Funktion schnappt sich den Eingangswert x und wirbelt seine binäre Darstellung (1100101…) durcheinander, um einen neuen Wert zu erzeugen.

1D: Interpolation

Als nächstes kommen die Interpolationen dran. Wir werden drei Arten einbauen: linear, cosine und cubic. In dieser Reihenfolge wird das Rauschen weicher, aber auch performancehungriger. Sprich: linear ist schnell, aber hässlich – cubic ist langsam, aber schön. Und cosine ist irgendwo in der Mitte. Für mehr Infos zu den Interpolationen verweise ich auf die eingangs erwähnten Artikel ;)

linear:

var getLinearInterpolation1D = function(a, b, x) {
    return a * (1 - x) + b * x;
};

cosine:

var getCosineInterpolation1D = function(a, b, x) {
    var ft = x * 3.1415927;
    ft = (1 - Math.cos(ft)) * 0.5;
    return a * (1 - ft) + b * ft;
};

cubic:

var getCubicInterpolation1D = function(v0, v1, v2, v3, x) {
    var P = (v3 - v2) - (v0 - v1);
    var Q = (v0 - v1) - P;
    var R = v2 - v0;
    var S = v1;

    return P * x * x * x + Q * x * x + R * x + S;
};

1D: Getter Funktion

Fein. Jetzt müssen wir uns eigentlich nur noch den Rauschwert an einer bestimmten Koordinate abholen. Je nach Interpolation sieht das so aus:

linear:

var getNoise1DLinear = function(x, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        pointPrev,
        pointAfter,
        xFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz = Math.floor(x * frequency);
        pointPrev = getNoise1D(xFrqz);
        pointAfter = getNoise1D(xFrqz + 1);
        value += getLinearInterpolation1D(pointPrev, pointAfter,
            x % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

cosine:

var getNoise1DCosine = function(x, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        pointPrev,
        pointAfter,
        xFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz = Math.floor(x * frequency);
        pointPrev = getNoise1D(xFrqz);
        pointAfter = getNoise1D(xFrqz + 1);
        value += getCosineInterpolation1D(pointPrev, pointAfter,
            x % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

cubic:

var getNoise1DCubic = function(x, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        pointPrev2,
        pointPrev1,
        pointAfter1,
        pointAfter2,
        xFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz = Math.floor(x * frequency);
        pointPrev2 = getNoise1D(xFrqz -1);
        pointPrev1 = getNoise1D(xFrqz);
        pointAfter1 = getNoise1D(xFrqz + 1);
        pointAfter2 = getNoise1D(xFrqz + 2);
        value += getCubicInterpolation1D(pointPrev2, pointPrev1, pointAfter1, pointAfter2,
            x % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

Nun nehmt ihr alle Codeschnipsel zusammen und bettet sie in die Objektstruktur ein:

(function() {

    // Code HIER einfügen

    window.Noizy = {
        getValueNoise1DLinear: getNoise1DLinear,
        getValueNoise1DCosine: getNoise1DCosine,
        getValueNoise1DCubic: getNoise1DCubic
    };
})();

Anschließend könnt ihr in einer Schleife diverse Zwischenwerte abfragen und euch diese anzeigen lassen.

Beispiel:

var drawNoise1D = function(canvas, noiseFunction, seedX, stepX, frequency,
                           amplitude, persistence, octaves) {
    var w = canvas.width;
    var h = canvas.height;
    var context  = canvas.getContext("2d");
    var imageData = context.createImageData(w, h);

    var height = [];
    for (var i = 0; i < w; i++) {
        var noise = noiseFunction(seedX + i * stepX, frequency, amplitude, persistence, octaves);
        height[i] = noise * 50 + 50;
    }

    for (var i = 0, l = imageData.data.length; i < l; i += 4) {
        var x = (i / 4) % w;
        var y = Math.floor(i / w / 4);

        var value = 0;

        if(y < height[x]) value = 255;

        imageData.data[i  ] = value;
        imageData.data[i + 1] = value;
        imageData.data[i + 2] = value;
        imageData.data[i + 3] = 255;
    }

    context.putImageData(imageData, 0, 0);
};

drawNoise1D(
    document.getElementById("valueNoise1DLinear"),  // canvas
    Noizy.getValueNoise1DLinear,
    126,    // seedX - Anfangskoordinate
    0.1,    // stepX - Zwischenschritte (pro Pixel seedX um stepX erhöhen)
    1,  // frequency,
    1,  // amplitude,
    1,  // persistence,
    1   // octaves
);

Und so könnt das Ganze aussehen (Livebeispiel):

Value Noise 1D

Value Noise 1D

Jetzt kommt 2D

Im 2D Bereich ähnelt sich alles, darum gehe ich hier etwas schneller durch, da viele 2D Funktionen auf 1D Funktionen zurückgreifen. Zuerst benötigen wir wieder pseudo-zufällige Werte:

var getNoise2D = function(x, y) {
    return getNoise1D(x * 46349111 + y * 46351111);
};

Die zwei Konstanten stellen übrigens hohe Primzahlen dar, die Wiederholungen im Rauschen verhindern sollen.

Anschließend die Interpolationsfunktionen:

var getLinearInterpolation2D = function(v00, v01, v10, v11, x, y) {
    var x0 = getLinearInterpolation1D(v00, v01, y);
    var x1 = getLinearInterpolation1D(v10, v11, y);
    return getLinearInterpolation1D(x0, x1, x);
};

var getCosineInterpolation2D = function(v00, v01, v10, v11, x, y) {
    var x0 = getCosineInterpolation1D(v00, v01, y);
    var x1 = getCosineInterpolation1D(v10, v11, y);
    return getCosineInterpolation1D(x0, x1, x);
};

var getCubicInterpolation2D = function(v_12, v02, v12, v22, v_11, v01, v11, v21, v_10,
                                  v00, v10, v20, v_1_1,v0_1, v1_1,v2_1, x, y) {
    var x_1 = getCubicInterpolation1D(v_1_1, v_10, v_11, v_12, y);
    var x0 = getCubicInterpolation1D(v0_1, v00, v01, v02, y);
    var x1 = getCubicInterpolation1D(v1_1, v10, v11, v12, y);
    var x2 = getCubicInterpolation1D(v2_1, v20, v21, v22, y);
    return getCubicInterpolation1D(x_1, x0, x1, x2, x);
};

Und zu guter letzt die Getter:

var getNoise2DLinear = function(x, y, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        v00,
        v01,
        v10,
        v11,
        xFrqz,
        yFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz  = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);
        v00 = getNoise2D(xFrqz, yFrqz);
        v01 = getNoise2D(xFrqz, yFrqz + 1);
        v10 = getNoise2D(xFrqz + 1, yFrqz);
        v11 = getNoise2D(xFrqz + 1, yFrqz + 1);
        value += getLinearInterpolation2D(v00, v01, v10, v11,
            x % (1 / frequency) * frequency,
            y % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

var getNoise2DCosine = function(x, y, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        v00,
        v01,
        v10,
        v11,
        xFrqz,
        yFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);
        v00 = getNoise2D(xFrqz, yFrqz);
        v01 = getNoise2D(xFrqz, yFrqz + 1);
        v10 = getNoise2D(xFrqz + 1, yFrqz);
        v11 = getNoise2D(xFrqz + 1, yFrqz + 1);
        value += getCosineInterpolation2D(v00, v01, v10, v11,
            x % (1 / frequency) * frequency, y % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

var getNoise2DCubic = function(x, y, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        xFrqz,
        yFrqz,
        v = [4];
    for (var i = 0; i < 4; i++) {
        v[i] = [4];
    }
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);

        v[0][3] = getNoise2D(xFrqz - 1, yFrqz + 2);
        v[0][2] = getNoise2D(xFrqz, yFrqz + 2);
        v[0][1] = getNoise2D(xFrqz + 1, yFrqz + 2);
        v[0][0] = getNoise2D(xFrqz + 2, yFrqz + 2);

        v[3][3] = getNoise2D(xFrqz - 1, yFrqz - 1);
        v[3][2] = getNoise2D(xFrqz, yFrqz - 1);
        v[3][1] = getNoise2D(xFrqz + 1, yFrqz - 1);
        v[3][0] = getNoise2D(xFrqz + 2, yFrqz - 1);

        v[1][3] = getNoise2D(xFrqz - 1, yFrqz + 1);
        v[1][2] = getNoise2D(xFrqz, yFrqz + 1);
        v[1][1] = getNoise2D(xFrqz + 1, yFrqz + 1);
        v[1][0] = getNoise2D(xFrqz + 2, yFrqz + 1);

        v[2][3] = getNoise2D(xFrqz - 1, yFrqz);
        v[2][2] = getNoise2D(xFrqz, yFrqz);
        v[2][1] = getNoise2D(xFrqz + 1, yFrqz);
        v[2][0] = getNoise2D(xFrqz + 2, yFrqz);

        value += getCubicInterpolation2D(v[0][3], v[0][2], v[0][1], v[0][0], v[1][3], v[1][2], v[1][1], v[1][0],
            v[2][3], v[2][2], v[2][1], v[2][0], v[3][3], v[3][2], v[3][1], v[3][0],
            x % (1 / frequency) * frequency, y % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

Das Wrappen nicht vergessen:

(function() {

    // 1D Code...

    // 2D Code HIER einfügen

    window.Noizy = {
        getValueNoise1DLinear: getNoise1DLinear,
        getValueNoise1DCosine: getNoise1DCosine,
        getValueNoise1DCubic: getNoise1DCubic,
        getValueNoise2DLinear: getNoise2DLinear,
        getValueNoise2DCosine: getNoise2DCosine,
        getValueNoise2DCubic: getNoise2DCubic
    };
})();

Und hier eine Funktion mit der ihr das Rauschen auf ein Canvas Element zeichnen könnt:

var drawNoise2D = function(canvas, noiseFunction, seedX, seedY, stepX, stepY,
                           frequency, amplitude, persistence, octaves) {
    var w = canvas.width;
    var h = canvas.height;
    var context  = canvas.getContext("2d");
    var imageData = context.createImageData(w, h);

    var map = [h];
    for (var y = 0; y < h; y++) {
           map[y] = [w];
            for (var x = 0; x < w; x++) {
                var noise = noiseFunction(seedX + x * stepX, seedY + y * stepY,
                    frequency, amplitude, persistence, octaves);
                map[y][x] = noise * 255;

            }
    }

    for (var i = 0, l = imageData.data.length; i < l; i += 4) {
        var x = (i / 4) % w;
        var y = Math.floor(i / w / 4);

        imageData.data[i  ] = map[y][x];
        imageData.data[i + 1] = map[y][x];
        imageData.data[i + 2] = map[y][x];
        imageData.data[i + 3] = 255;
    }

    context.putImageData(imageData, 0, 0);
};

drawNoise2D(
    document.getElementById("valueNoise2DLinear"),  // canvas
    Noizy.getValueNoise2DLinear,
    126,    // seedX,
    11,     // seedY,
    0.1,    // stepX,
    0.1,    // stepY,
    1,  // frequency,
    1,  // amplitude,
    1,  // persistence,
    1   // octaves
);

Das ist unser Ergebnis (Livebeispiel):

Value Noise 2D

Value Noise 2D

Und zum Schluss 3D

Wie zuvor. Pseudo-zufällige Werte:

var getNoise3D = function(x, y, z) {
    return getNoise1D(x * 1663 + y * 1667 + z * 1669);
};

Interpolation:

var getLinearInterpolation3D = function(v000, v010, v100, v110, v001, v011, v101, v111, x, y, z) {
    var x0 = getLinearInterpolation1D(v000, v010, y);
    var x1 = getLinearInterpolation1D(v100, v110, y);
    var x2 = getLinearInterpolation1D(v001, v011, y);
    var x3 = getLinearInterpolation1D(v101, v111, y);
    var y0 = getLinearInterpolation1D(x0, x1, x);
    var y1 = getLinearInterpolation1D(x2, x3, x);
    return getLinearInterpolation1D(y0, y1, z);
};

var getCosineInterpolation3D = function(v000, v010, v100, v110, v001, v011, v101, v111, x, y, z) {
    var x0 = getCosineInterpolation1D(v000, v010, y);
    var x1 = getCosineInterpolation1D(v100, v110, y);
    var x2 = getCosineInterpolation1D(v001, v011, y);
    var x3 = getCosineInterpolation1D(v101, v111, y);
    var y0 = getCosineInterpolation1D(x0, x1, x);
    var y1 = getCosineInterpolation1D(x2, x3, x);
    return getCosineInterpolation1D(y0, y1, z);
};

var getCubicInterpolation3D = function(values, x, y, z) {
    var xy_1_1 = getCubicInterpolation1D(values[0][0][0], values[0][0][1], values[0][0][2], values[0][0][3], z);
    var xy_10 = getCubicInterpolation1D(values[0][1][0], values[0][1][1], values[0][1][2], values[0][1][3], z);
    var xy_11 = getCubicInterpolation1D(values[0][2][0], values[0][2][1], values[0][2][2], values[0][2][3], z);
    var xy_12 = getCubicInterpolation1D(values[0][3][0], values[0][3][1], values[0][3][2], values[0][3][3], z);
    var xy0_1 = getCubicInterpolation1D(values[1][0][0], values[1][0][1], values[1][0][2], values[1][0][3], z);
    var xy00 = getCubicInterpolation1D(values[1][1][0], values[1][1][1], values[1][0][2], values[1][1][3], z);
    var xy01 = getCubicInterpolation1D(values[1][2][0], values[1][2][1], values[1][0][2], values[1][2][3], z);
    var xy02 = getCubicInterpolation1D(values[1][3][0], values[1][3][1], values[1][0][2], values[1][3][3], z);
    var xy1_1 = getCubicInterpolation1D(values[2][0][0], values[2][0][1], values[2][0][2], values[2][0][3], z);
    var xy10 = getCubicInterpolation1D(values[2][1][0], values[2][1][1], values[2][0][2], values[2][1][3], z);
    var xy11 = getCubicInterpolation1D(values[2][2][0], values[2][2][1], values[2][0][2], values[2][2][3], z);
    var xy12 = getCubicInterpolation1D(values[2][3][0], values[2][3][1], values[2][0][2], values[2][3][3], z);
    var xy2_1 = getCubicInterpolation1D(values[3][0][0], values[3][0][1], values[3][0][2], values[3][0][3], z);
    var xy20 = getCubicInterpolation1D(values[3][1][0], values[3][1][1], values[3][0][2], values[3][1][3], z);
    var xy21 = getCubicInterpolation1D(values[3][2][0], values[3][2][1], values[3][0][2], values[3][2][3], z);
    var xy22 = getCubicInterpolation1D(values[3][3][0], values[3][3][1], values[3][0][2], values[3][3][3], z);
    var x_1 = getCubicInterpolation1D(xy_1_1, xy_10, xy_11, xy_12, y);
    var x0 = getCubicInterpolation1D(xy0_1, xy00, xy01, xy02, y);
    var x1 = getCubicInterpolation1D(xy1_1, xy10, xy11, xy12, y);
    var x2 = getCubicInterpolation1D(xy2_1, xy20, xy21, xy22, y);
    return getCubicInterpolation1D(x_1, x0, x1, x2, x);
};

Getter:

var getNoise3DLinear = function(x, y, z, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        v000,
        v010,
        v100,
        v110,
        v001,
        v011,
        v101,
        v111,
        xFrqz,
        yFrqz,
        zFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz  = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);
        zFrqz = Math.floor(z * frequency);
        v000 = getNoise3D(xFrqz, yFrqz, zFrqz);
        v010 = getNoise3D(xFrqz, yFrqz + 1, zFrqz);
        v100 = getNoise3D(xFrqz + 1, yFrqz, zFrqz);
        v110 = getNoise3D(xFrqz + 1, yFrqz + 1, zFrqz);
        v001 = getNoise3D(xFrqz, yFrqz, zFrqz + 1);
        v011 = getNoise3D(xFrqz, yFrqz + 1, zFrqz + 1);
        v101 = getNoise3D(xFrqz + 1, yFrqz, zFrqz + 1);
        v111 = getNoise3D(xFrqz + 1, yFrqz + 1, zFrqz + 1);
        value += getLinearInterpolation3D(v000, v010, v100, v110, v001, v011, v101, v111,
            x % (1 / frequency) * frequency,
            y % (1 / frequency) * frequency,
            z % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

var getNoise3DCosine = function(x, y, z, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        v000,
        v010,
        v100,
        v110,
        v001,
        v011,
        v101,
        v111,
        xFrqz,
        yFrqz,
        zFrqz;
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz  = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);
        zFrqz = Math.floor(z * frequency);
        v000 = getNoise3D(xFrqz, yFrqz, zFrqz);
        v010 = getNoise3D(xFrqz, yFrqz + 1, zFrqz);
        v100 = getNoise3D(xFrqz + 1, yFrqz, zFrqz);
        v110 = getNoise3D(xFrqz + 1, yFrqz + 1, zFrqz);
        v001 = getNoise3D(xFrqz, yFrqz, zFrqz + 1);
        v011 = getNoise3D(xFrqz, yFrqz + 1, zFrqz + 1);
        v101 = getNoise3D(xFrqz + 1, yFrqz, zFrqz + 1);
        v111 = getNoise3D(xFrqz + 1, yFrqz + 1, zFrqz + 1);
        value += getCosineInterpolation3D(v000, v010, v100, v110, v001, v011, v101, v111,
            x % (1 / frequency) * frequency,
            y % (1 / frequency) * frequency,
            z % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

var getNoise3DCubic = function(x, y, z, frequency, amplitude, persistence, octaves)
{
    var value = 0,
        xFrqz,
        yFrqz,
        zFrqz,
        v = [4];
    for (var i = 0; i < 4; i++) {
        v[i] = [4];
        for (var j = 0; j < 4; j++) {
            v[i][j] = [4];
        }
    }
    frequency = 1 / frequency;
    for (var i = 0; i < octaves; i++)
    {
        xFrqz  = Math.floor(x * frequency);
        yFrqz = Math.floor(y * frequency);
        zFrqz = Math.floor(z * frequency);
        for (var vx = 0; vx < 4; vx++)
        {
            for (var vy = 0; vy < 4; vy++)
            {
                for (var vz = 0; vz < 4; vz++)
                {
                    v[vx][vy][vz] = getNoise3D(xFrqz + vx - 1, yFrqz + vy - 1, zFrqz + vz - 1);
                }
            }
        }
        value += getCubicInterpolation3D(v,
            x % (1 / frequency) * frequency,
            y % (1 / frequency) * frequency,
            z % (1 / frequency) * frequency) * amplitude;
        frequency *= 2;
        amplitude *= persistence;
    }
    return value;
};

Wrappen:

(function() {

    // 1D und 2D Code...

    // 3D Code HIER einfügen

    window.Noizy = {
        getValueNoise1DLinear: getNoise1DLinear,
        getValueNoise1DCosine: getNoise1DCosine,
        getValueNoise1DCubic: getNoise1DCubic,
        getValueNoise2DLinear: getNoise2DLinear,
        getValueNoise2DCosine: getNoise2DCosine,
        getValueNoise2DCubic: getNoise2DCubic,
        getValueNoise3DLinear: getNoise3DLinear,
        getValueNoise3DCosine: getNoise3DCosine,
        getValueNoise3DCubic: getNoise3DCubic
    };
})();

Zeichen-Funktion:

var drawNoise3D = function(canvas, noiseFunction, seedX, seedY, seedZ, stepX, stepY, stepZ,
                           frequency, amplitude, persistence, octaves) {
    var w = canvas.width;
    var h = canvas.height;
    var context  = canvas.getContext("2d");
    var imageData = context.createImageData(w, h);

    var cubeSize = 100;

    var map = [cubeSize];
    for (var z = 0; z < cubeSize; z++) {
        map[z] = [cubeSize];
        for (var y = 0; y < cubeSize; y++) {
            map[z][y] = [cubeSize];
            for (var x = 0; x < cubeSize; x++) {
                map[z][y][x] = noiseFunction(seedX + x * stepX, seedY + y * stepY, seedZ + z * stepZ,
                    frequency, amplitude, persistence, octaves) * 255;
            }
        }
    }

    var offset = 10;
    for(var z = 0; z < cubeSize / 2; z++) {
        for (var i = 0, l = imageData.data.length; i < l; i += 4) {
            var x = (i / 4) % w;
            var y = Math.floor(i / w / 4);
            var shadowOrLight = 0;

            if((x >= offset && y >= offset) &&
                (cubeSize > x - offset && cubeSize > y - offset)) {
                if(y - offset == 0) shadowOrLight += 50;
                if(x - offset == 0) shadowOrLight -= 50;
                var value = map[z * 2][y - offset][x - offset] + shadowOrLight;
                imageData.data[i  ] = value;
                imageData.data[i + 1] = value;
                imageData.data[i + 2] = value;
                imageData.data[i + 3] = 255;
            }
        }

        context.putImageData(imageData, 0, 0);

        offset++;
    }
};

drawNoise3D(
    document.getElementById("valueNoise3DLinear"),  // canvas
    Noizy.getValueNoise3DLinear,
    126,    // seedX,
    11,     // seedY,
    17,     // seedZ,
    0.1,    // stepX,
    0.1,    // stepY,
    0.1,    // stepZ,
    1,  // frequency,
    1,  // amplitude,
    1,  // persistence,
    1   // octaves
);

Ergebnis (LivebeispielAchtung, langsam!):

Value Noise 3D

Value Noise 3D

Aber hoppla! Was ist bei 3D Cubic los? Man kann hier ganz klar Artefakte erkennen. Leider konnte ich nicht herausfinden, wo sie herkommen. Aber bei senäh handelt es sich ja durchaus auch um einen Mitmach-Blog :D Frei nach dem Motto: “Finde den Fehler!” würde ich mich freuen, wenn jemand den Artefakten auf die Schliche kommt, um den Bug in meinem Code findet, der sie verursacht.

Fazit

Fein, wir haben unsere erste prototypische Implementierung, welche auch schon brauchbare Ergebnisse liefert, aber auch noch viel Platz nach oben bietet. So ist die 3D Performance durchaus “bescheiden” und die Artefakte mit 3D Cubic Interpolation auch nicht schön. Außerdem zeichnet sich ein riesiges Code-Chaos ab, wenn wir zusätzlich Gradient Noise und Simplex Noise implementieren. Wir müssen also irgendwie unseren Code besser strukturieren. Puuh!

Hier findet ihr die komplette Noizy Bibliothek in Version r1.

Ganz schön viel zu tun bis zum nächsten Mal. Na dann, happy coding bis dahin ;)

Autor: Pipo

...kommt ursprünglich aus der Designerecke, ist aber im Laufe seines Studiums in die Webentwicklung gestolpert. Kann sich seit dem nicht so richtig entscheiden wo er hingehört und wagt deswegen vielleicht die Flucht nach vorne in ein komplett neues Gebiet. Vielleicht Management? Niemand weiß es. Auch er nicht.

Kommentare (2)

  1. Was, keine Kommentare? Diese Serie ist 1A! Die Theorie verstehe zwar super, aber der Code ist trotzdem ägyptisch für mich. Egal, ich müsste mich halt noch etwas mehr damit beschäftigen.
    Schade, dass es nicht bis zu Simplex ging.

    • Haha, danke^^

      Gradient Noise und Simplex Noise haben es leider wirklich nicht in Blogform geschafft, sind aber hier als GitHub Projekt zu finden. Gerade bei letzterem bin ich aber leider auch vom Verständnis her nicht mehr mitgekommen und habe lediglich irgendwelche C++-Algorithmen nach JavaScript portiert. Darum der fehlende Blogpost…

      Falls es dich noch interessiert: Die beste Simplex Noise Implementierung in JavaScript, die ich kenne, ist diese hier.