From 542abe913cbe3b6dab4f67aad92e8df472da357e Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Tue, 11 Feb 2025 04:05:07 +0100 Subject: [PATCH 1/3] commit --- simplex-noise.ts | 184 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/simplex-noise.ts b/simplex-noise.ts index 188e62a..3919ce9 100644 --- a/simplex-noise.ts +++ b/simplex-noise.ts @@ -326,6 +326,190 @@ export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D { }; } + +/** + * A function type that returns both the noise value and its derivative. + */ +export type NoiseDeriv3D = (x: number, y: number, z: number) => { + /** The noise value in the interval [-1,1]. */ + value: number; + /** The derivative with respect to x. */ + dx: number; + /** The derivative with respect to y. */ + dy: number; + /** The derivative with respect to z. */ + dz: number; +}; + +/** + * Creates a 3D simplex noise function that also computes its analytical derivative. + * + * The returned function, when called with coordinates (x, y, z), returns an object with the noise value + * and its derivative (dx, dy, dz). (Note: the derivative is computed assuming that the integer cell indices + * remain constant—so at the boundaries between cells the derivative is discontinuous.) + * + * @param random a random function returning numbers in [0,1); defaults to Math.random + * @returns {NoiseDeriv3D} + */ +export function createNoise3DWithDerivatives(random: RandomFn = Math.random): NoiseDeriv3D { + const perm = buildPermutationTable(random); + // Precompute gradient components for speed – just like in createNoise3D. + const permGrad3x = new Float64Array(perm).map(v => grad3[(v % 12) * 3]); + const permGrad3y = new Float64Array(perm).map(v => grad3[(v % 12) * 3 + 1]); + const permGrad3z = new Float64Array(perm).map(v => grad3[(v % 12) * 3 + 2]); + + return function noise3DWithDerivatives(x: number, y: number, z: number) { + // Skew the input space to determine which simplex cell we're in + const s = (x + y + z) * F3; + const i = fastFloor(x + s); + const j = fastFloor(y + s); + const k = fastFloor(z + s); + const t = (i + j + k) * G3; + const X0 = i - t; + const Y0 = j - t; + const Z0 = k - t; + const x0 = x - X0; + const y0 = y - Y0; + const z0 = z - Z0; + + // Determine which simplex we are in. + let i1: number, j1: number, k1: number; + let i2: number, j2: number, k2: number; + if (x0 >= y0) { + if (y0 >= z0) { + // X Y Z order + i1 = 1; j1 = 0; k1 = 0; + i2 = 1; j2 = 1; k2 = 0; + } else if (x0 >= z0) { + // X Z Y order + i1 = 1; j1 = 0; k1 = 0; + i2 = 1; j2 = 0; k2 = 1; + } else { + // Z X Y order + i1 = 0; j1 = 0; k1 = 1; + i2 = 1; j2 = 0; k2 = 1; + } + } else { + // x0 < y0 + if (y0 < z0) { + // Z Y X order + i1 = 0; j1 = 0; k1 = 1; + i2 = 0; j2 = 1; k2 = 1; + } else if (x0 < z0) { + // Y Z X order + i1 = 0; j1 = 1; k1 = 0; + i2 = 0; j2 = 1; k2 = 1; + } else { + // Y X Z order + i1 = 0; j1 = 1; k1 = 0; + i2 = 1; j2 = 1; k2 = 0; + } + } + + // Offsets for remaining corners + const x1 = x0 - i1 + G3; + const y1 = y0 - j1 + G3; + const z1 = z0 - k1 + G3; + const x2 = x0 - i2 + 2.0 * G3; + const y2 = y0 - j2 + 2.0 * G3; + const z2 = z0 - k2 + 2.0 * G3; + const x3 = x0 - 1.0 + 3.0 * G3; + const y3 = y0 - 1.0 + 3.0 * G3; + const z3 = z0 - 1.0 + 3.0 * G3; + + // Wrap the integer indices at 256, same as in the other noise functions + const ii = i & 255; + const jj = j & 255; + const kk = k & 255; + + // Initialize accumulators for noise and derivative contributions + let n0 = 0, n1 = 0, n2 = 0, n3 = 0; + let d0x = 0, d0y = 0, d0z = 0; + let d1x = 0, d1y = 0, d1z = 0; + let d2x = 0, d2y = 0, d2z = 0; + let d3x = 0, d3y = 0, d3z = 0; + + // Helper: for each corner compute contribution if within radius. + // The contribution is n = t^4 * (g·(offset)), + // and the derivative with respect to x is: dn/dx = t^4 * g_x – 8 * x_i * t^3 * (g·(offset)), + // with analogous formulas for y and z. + // + // Corner 0: + const t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; + if (t0 > 0) { + const t0_2 = t0 * t0; + const t0_4 = t0_2 * t0_2; + const gi0 = ii + perm[jj + perm[kk]]; + const g0x = permGrad3x[gi0]; + const g0y = permGrad3y[gi0]; + const g0z = permGrad3z[gi0]; + const dot0 = g0x * x0 + g0y * y0 + g0z * z0; + n0 = t0_4 * dot0; + d0x = t0_4 * g0x - 8 * x0 * (t0 * t0_2) * dot0; + d0y = t0_4 * g0y - 8 * y0 * (t0 * t0_2) * dot0; + d0z = t0_4 * g0z - 8 * z0 * (t0 * t0_2) * dot0; + } + + // Corner 1: + const t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; + if (t1 > 0) { + const t1_2 = t1 * t1; + const t1_4 = t1_2 * t1_2; + const gi1 = ii + i1 + perm[jj + j1 + perm[kk + k1]]; + const g1x = permGrad3x[gi1]; + const g1y = permGrad3y[gi1]; + const g1z = permGrad3z[gi1]; + const dot1 = g1x * x1 + g1y * y1 + g1z * z1; + n1 = t1_4 * dot1; + d1x = t1_4 * g1x - 8 * x1 * (t1 * t1_2) * dot1; + d1y = t1_4 * g1y - 8 * y1 * (t1 * t1_2) * dot1; + d1z = t1_4 * g1z - 8 * z1 * (t1 * t1_2) * dot1; + } + + // Corner 2: + const t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; + if (t2 > 0) { + const t2_2 = t2 * t2; + const t2_4 = t2_2 * t2_2; + const gi2 = ii + i2 + perm[jj + j2 + perm[kk + k2]]; + const g2x = permGrad3x[gi2]; + const g2y = permGrad3y[gi2]; + const g2z = permGrad3z[gi2]; + const dot2 = g2x * x2 + g2y * y2 + g2z * z2; + n2 = t2_4 * dot2; + d2x = t2_4 * g2x - 8 * x2 * (t2 * t2_2) * dot2; + d2y = t2_4 * g2y - 8 * y2 * (t2 * t2_2) * dot2; + d2z = t2_4 * g2z - 8 * z2 * (t2 * t2_2) * dot2; + } + + // Corner 3: + const t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; + if (t3 > 0) { + const t3_2 = t3 * t3; + const t3_4 = t3_2 * t3_2; + const gi3 = ii + 1 + perm[jj + 1 + perm[kk + 1]]; + const g3x = permGrad3x[gi3]; + const g3y = permGrad3y[gi3]; + const g3z = permGrad3z[gi3]; + const dot3 = g3x * x3 + g3y * y3 + g3z * z3; + n3 = t3_4 * dot3; + d3x = t3_4 * g3x - 8 * x3 * (t3 * t3_2) * dot3; + d3y = t3_4 * g3y - 8 * y3 * (t3 * t3_2) * dot3; + d3z = t3_4 * g3z - 8 * z3 * (t3 * t3_2) * dot3; + } + + // Sum up contributions and apply the final scaling factor. + // (As in createNoise3D, the noise is multiplied by 32.) + const noise = 32.0 * (n0 + n1 + n2 + n3); + const dx = 32.0 * (d0x + d1x + d2x + d3x); + const dy = 32.0 * (d0y + d1y + d2y + d3y); + const dz = 32.0 * (d0z + d1z + d2z + d3z); + + return { value: noise, dx, dy, dz }; + }; +} + + /** * Samples the noise field in four dimensions * From 052dfd0f55fe6dabfa856539ba9dfa0fea86cdea Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:56:57 +0100 Subject: [PATCH 2/3] complete 2,3,4d derivatives + tests --- simplex-noise.ts | 914 +++++++++++++++++----- snapshots/noise3DlargeWithDerivatives.png | Bin 0 -> 16521 bytes snapshots/noise3DsmallWithDerivatives.png | Bin 0 -> 12231 bytes snapshots/noise4DlargeWithDerivatives.png | Bin 0 -> 16521 bytes snapshots/noise4DsmallWithDerivatives.png | Bin 0 -> 12105 bytes test/matches-snapshot.ts | 69 ++ test/simplex-noise-test.ts | 280 ++++++- 7 files changed, 1075 insertions(+), 188 deletions(-) create mode 100644 snapshots/noise3DlargeWithDerivatives.png create mode 100644 snapshots/noise3DsmallWithDerivatives.png create mode 100644 snapshots/noise4DlargeWithDerivatives.png create mode 100644 snapshots/noise4DsmallWithDerivatives.png diff --git a/simplex-noise.ts b/simplex-noise.ts index 3919ce9..255a0ca 100644 --- a/simplex-noise.ts +++ b/simplex-noise.ts @@ -326,190 +326,6 @@ export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D { }; } - -/** - * A function type that returns both the noise value and its derivative. - */ -export type NoiseDeriv3D = (x: number, y: number, z: number) => { - /** The noise value in the interval [-1,1]. */ - value: number; - /** The derivative with respect to x. */ - dx: number; - /** The derivative with respect to y. */ - dy: number; - /** The derivative with respect to z. */ - dz: number; -}; - -/** - * Creates a 3D simplex noise function that also computes its analytical derivative. - * - * The returned function, when called with coordinates (x, y, z), returns an object with the noise value - * and its derivative (dx, dy, dz). (Note: the derivative is computed assuming that the integer cell indices - * remain constant—so at the boundaries between cells the derivative is discontinuous.) - * - * @param random a random function returning numbers in [0,1); defaults to Math.random - * @returns {NoiseDeriv3D} - */ -export function createNoise3DWithDerivatives(random: RandomFn = Math.random): NoiseDeriv3D { - const perm = buildPermutationTable(random); - // Precompute gradient components for speed – just like in createNoise3D. - const permGrad3x = new Float64Array(perm).map(v => grad3[(v % 12) * 3]); - const permGrad3y = new Float64Array(perm).map(v => grad3[(v % 12) * 3 + 1]); - const permGrad3z = new Float64Array(perm).map(v => grad3[(v % 12) * 3 + 2]); - - return function noise3DWithDerivatives(x: number, y: number, z: number) { - // Skew the input space to determine which simplex cell we're in - const s = (x + y + z) * F3; - const i = fastFloor(x + s); - const j = fastFloor(y + s); - const k = fastFloor(z + s); - const t = (i + j + k) * G3; - const X0 = i - t; - const Y0 = j - t; - const Z0 = k - t; - const x0 = x - X0; - const y0 = y - Y0; - const z0 = z - Z0; - - // Determine which simplex we are in. - let i1: number, j1: number, k1: number; - let i2: number, j2: number, k2: number; - if (x0 >= y0) { - if (y0 >= z0) { - // X Y Z order - i1 = 1; j1 = 0; k1 = 0; - i2 = 1; j2 = 1; k2 = 0; - } else if (x0 >= z0) { - // X Z Y order - i1 = 1; j1 = 0; k1 = 0; - i2 = 1; j2 = 0; k2 = 1; - } else { - // Z X Y order - i1 = 0; j1 = 0; k1 = 1; - i2 = 1; j2 = 0; k2 = 1; - } - } else { - // x0 < y0 - if (y0 < z0) { - // Z Y X order - i1 = 0; j1 = 0; k1 = 1; - i2 = 0; j2 = 1; k2 = 1; - } else if (x0 < z0) { - // Y Z X order - i1 = 0; j1 = 1; k1 = 0; - i2 = 0; j2 = 1; k2 = 1; - } else { - // Y X Z order - i1 = 0; j1 = 1; k1 = 0; - i2 = 1; j2 = 1; k2 = 0; - } - } - - // Offsets for remaining corners - const x1 = x0 - i1 + G3; - const y1 = y0 - j1 + G3; - const z1 = z0 - k1 + G3; - const x2 = x0 - i2 + 2.0 * G3; - const y2 = y0 - j2 + 2.0 * G3; - const z2 = z0 - k2 + 2.0 * G3; - const x3 = x0 - 1.0 + 3.0 * G3; - const y3 = y0 - 1.0 + 3.0 * G3; - const z3 = z0 - 1.0 + 3.0 * G3; - - // Wrap the integer indices at 256, same as in the other noise functions - const ii = i & 255; - const jj = j & 255; - const kk = k & 255; - - // Initialize accumulators for noise and derivative contributions - let n0 = 0, n1 = 0, n2 = 0, n3 = 0; - let d0x = 0, d0y = 0, d0z = 0; - let d1x = 0, d1y = 0, d1z = 0; - let d2x = 0, d2y = 0, d2z = 0; - let d3x = 0, d3y = 0, d3z = 0; - - // Helper: for each corner compute contribution if within radius. - // The contribution is n = t^4 * (g·(offset)), - // and the derivative with respect to x is: dn/dx = t^4 * g_x – 8 * x_i * t^3 * (g·(offset)), - // with analogous formulas for y and z. - // - // Corner 0: - const t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; - if (t0 > 0) { - const t0_2 = t0 * t0; - const t0_4 = t0_2 * t0_2; - const gi0 = ii + perm[jj + perm[kk]]; - const g0x = permGrad3x[gi0]; - const g0y = permGrad3y[gi0]; - const g0z = permGrad3z[gi0]; - const dot0 = g0x * x0 + g0y * y0 + g0z * z0; - n0 = t0_4 * dot0; - d0x = t0_4 * g0x - 8 * x0 * (t0 * t0_2) * dot0; - d0y = t0_4 * g0y - 8 * y0 * (t0 * t0_2) * dot0; - d0z = t0_4 * g0z - 8 * z0 * (t0 * t0_2) * dot0; - } - - // Corner 1: - const t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; - if (t1 > 0) { - const t1_2 = t1 * t1; - const t1_4 = t1_2 * t1_2; - const gi1 = ii + i1 + perm[jj + j1 + perm[kk + k1]]; - const g1x = permGrad3x[gi1]; - const g1y = permGrad3y[gi1]; - const g1z = permGrad3z[gi1]; - const dot1 = g1x * x1 + g1y * y1 + g1z * z1; - n1 = t1_4 * dot1; - d1x = t1_4 * g1x - 8 * x1 * (t1 * t1_2) * dot1; - d1y = t1_4 * g1y - 8 * y1 * (t1 * t1_2) * dot1; - d1z = t1_4 * g1z - 8 * z1 * (t1 * t1_2) * dot1; - } - - // Corner 2: - const t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; - if (t2 > 0) { - const t2_2 = t2 * t2; - const t2_4 = t2_2 * t2_2; - const gi2 = ii + i2 + perm[jj + j2 + perm[kk + k2]]; - const g2x = permGrad3x[gi2]; - const g2y = permGrad3y[gi2]; - const g2z = permGrad3z[gi2]; - const dot2 = g2x * x2 + g2y * y2 + g2z * z2; - n2 = t2_4 * dot2; - d2x = t2_4 * g2x - 8 * x2 * (t2 * t2_2) * dot2; - d2y = t2_4 * g2y - 8 * y2 * (t2 * t2_2) * dot2; - d2z = t2_4 * g2z - 8 * z2 * (t2 * t2_2) * dot2; - } - - // Corner 3: - const t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; - if (t3 > 0) { - const t3_2 = t3 * t3; - const t3_4 = t3_2 * t3_2; - const gi3 = ii + 1 + perm[jj + 1 + perm[kk + 1]]; - const g3x = permGrad3x[gi3]; - const g3y = permGrad3y[gi3]; - const g3z = permGrad3z[gi3]; - const dot3 = g3x * x3 + g3y * y3 + g3z * z3; - n3 = t3_4 * dot3; - d3x = t3_4 * g3x - 8 * x3 * (t3 * t3_2) * dot3; - d3y = t3_4 * g3y - 8 * y3 * (t3 * t3_2) * dot3; - d3z = t3_4 * g3z - 8 * z3 * (t3 * t3_2) * dot3; - } - - // Sum up contributions and apply the final scaling factor. - // (As in createNoise3D, the noise is multiplied by 32.) - const noise = 32.0 * (n0 + n1 + n2 + n3); - const dx = 32.0 * (d0x + d1x + d2x + d3x); - const dy = 32.0 * (d0y + d1y + d2y + d3y); - const dz = 32.0 * (d0z + d1z + d2z + d3z); - - return { value: noise, dx, dy, dz }; - }; -} - - /** * Samples the noise field in four dimensions * @@ -681,4 +497,732 @@ export function buildPermutationTable(random: RandomFn): Uint8Array { p[i] = p[i - 256]; } return p; -} \ No newline at end of file +} + + +/** + * The output of the noise function with derivatives + * + * @property value the noise value in the interval [-1, 1] + * @property dx the partial derivative with respect to x + * @property dy the partial derivative with respect to y + */ +export type NoiseDeriv2DOutput = { + value: number; + dx: number; + dy: number; +}; + +/** + * Samples the noise field in two dimensions and returns the partial derivatives (dx, dy). + * + * Coordinates should be finite, bigger than -2^31 and smaller than 2^31. + * @param x + * @param y + * @param output optional output object to store the result, if not provided, a new one will be created + * @returns {NoiseDeriv2DOutput} + */ +export type NoiseDerivFunction2D = (x: number, y: number, output?: NoiseDeriv2DOutput) => NoiseDeriv2DOutput; + +/** + * Creates a 2D simplex noise function that also returns the analytical derivatives: + * value = noise(x, y) + * dx = ∂(noise)/∂x + * dy = ∂(noise)/∂y + * + * Returns a function that takes (x, y) and returns { value, dx, dy }. + * @param random the random function that will be used to build the permutation table + * @returns {NoiseDerivFunction2D} + */ +export function createNoise2DWithDerivatives(random: RandomFn = Math.random): NoiseDerivFunction2D { + const perm = buildPermutationTable(random); + // Precompute the x/y gradients for each possible permutation value + const permGrad2x = new Float64Array(perm).map(v => grad2[(v % 12) * 2]); + const permGrad2y = new Float64Array(perm).map(v => grad2[(v % 12) * 2 + 1]); + + return function noise2DWithDerivatives(x: number, y: number, output?: NoiseDeriv2DOutput): NoiseDeriv2DOutput { + // Noise and derivatives that we'll accumulate + let value = 0; + let dx = 0; + let dy = 0; + + // Skew the input space to determine which simplex cell we're in + const s = (x + y) * F2; + const i = fastFloor(x + s); + const j = fastFloor(y + s); + const t = (i + j) * G2; + const X0 = i - t; + const Y0 = j - t; + + // Unskewed distances from the cell origin + const x0 = x - X0; + const y0 = y - Y0; + + // Determine which simplex triangle we are in + let i1, j1; + if (x0 > y0) { + i1 = 1; j1 = 0; + } else { + i1 = 0; j1 = 1; + } + + // Offsets for the other two corners + const x1 = x0 - i1 + G2; + const y1 = y0 - j1 + G2; + const x2 = x0 - 1.0 + 2.0 * G2; + const y2 = y0 - 1.0 + 2.0 * G2; + + // Work out the hashed gradient indices of the three corners + const ii = i & 255; + const jj = j & 255; + + // -- Corner 0 + const t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 > 0) { + // Precompute the gradient index and the actual gradient + const gi0 = ii + perm[jj]; + const g0x = permGrad2x[gi0]; + const g0y = permGrad2y[gi0]; + + // Contribution (the usual simplex noise formula) + const t0sq = t0 * t0; // t0^2 + const t0p4 = t0sq * t0sq; // t0^4 + const dot0 = g0x * x0 + g0y * y0; + const n0 = t0p4 * dot0; + + // Derivatives: + // + // t0 = 0.5 - x0^2 - y0^2 + // dt0/dx = -2 * x0 + // dt0/dy = -2 * y0 + // + // ∂n0/∂x = ∂/∂x [ t0^4 * (g0x*x0 + g0y*y0) ] + // = (4 * t0^3 * dt0/dx) * (g0x*x0 + g0y*y0) + // + t0^4 * g0x * (∂x0/∂x) + // Since x0 = x - X0, (∂x0/∂x) = 1 + // + // Putting it together: + const t0cubic = t0sq * t0; // t0^3 + const dT0dx = -2.0 * x0; + const dT0dy = -2.0 * y0; + + const dn0dx = 4.0 * t0cubic * dT0dx * dot0 + t0p4 * g0x; + const dn0dy = 4.0 * t0cubic * dT0dy * dot0 + t0p4 * g0y; + + // Accumulate + value += n0; + dx += dn0dx; + dy += dn0dy; + } + + // -- Corner 1 + const t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 > 0) { + const gi1 = ii + i1 + perm[jj + j1]; + const g1x = permGrad2x[gi1]; + const g1y = permGrad2y[gi1]; + + const t1sq = t1 * t1; + const t1p4 = t1sq * t1sq; + const dot1 = g1x * x1 + g1y * y1; + const n1 = t1p4 * dot1; + + const t1cubic = t1sq * t1; + const dT1dx = -2.0 * x1; // because x1 depends on x + const dT1dy = -2.0 * y1; // because y1 depends on y + + const dn1dx = 4.0 * t1cubic * dT1dx * dot1 + t1p4 * g1x; + const dn1dy = 4.0 * t1cubic * dT1dy * dot1 + t1p4 * g1y; + + value += n1; + dx += dn1dx; + dy += dn1dy; + } + + // -- Corner 2 + const t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 > 0) { + const gi2 = (ii + 1) + perm[jj + 1]; + const g2x = permGrad2x[gi2]; + const g2y = permGrad2y[gi2]; + + const t2sq = t2 * t2; + const t2p4 = t2sq * t2sq; + const dot2 = g2x * x2 + g2y * y2; + const n2 = t2p4 * dot2; + + const t2cubic = t2sq * t2; + const dT2dx = -2.0 * x2; + const dT2dy = -2.0 * y2; + + const dn2dx = 4.0 * t2cubic * dT2dx * dot2 + t2p4 * g2x; + const dn2dy = 4.0 * t2cubic * dT2dy * dot2 + t2p4 * g2y; + + value += n2; + dx += dn2dx; + dy += dn2dy; + } + + // Scale the final result (the same factor 70 used in standard 2D simplex) + value *= 70.0; + dx *= 70.0; + dy *= 70.0; + + if (output) { + output.value = value; + output.dx = dx; + output.dy = dy; + return output; + } + + return { value, dx, dy }; + }; +} + +/** + * The output of the noise function with derivatives + * + * @property value the noise value in the interval [-1, 1] + * @property dx the partial derivative with respect to x + * @property dy the partial derivative with respect to y + * @property dz the partial derivative with respect to z + */ +export type NoiseDeriv3DOutput = { + value: number; + dx: number; + dy: number; + dz: number; +}; + +/** + * Samples the noise field in three dimensions and returns the partial derivatives (dx, dy, dz). + * + * Coordinates should be finite, bigger than -2^31 and smaller than 2^31. + * @param x + * @param y + * @param z + * @param output + * @returns a number in the interval [-1, 1] + */ +export type NoiseDerivFunction3D = (x: number, y: number, z: number, output?: NoiseDeriv3DOutput) => NoiseDeriv3DOutput; + + +/** + * Creates a 3D Simplex noise function that also returns partial derivatives (dx, dy, dz). + * + * The final noise value is scaled to ~[-1, 1], just like `createNoise3D`. + * The derivatives match that same scaling. + * + * @param random the random function that will be used to build the permutation table + * @returns {NoiseDerivFunction3D} + */ +export function createNoise3DWithDerivatives( + random: RandomFn = Math.random +): NoiseDerivFunction3D { + // Build the permutation table + const perm = buildPermutationTable(random); + + // Precompute the 3D gradients for each possible perm value + // i.e. permGrad3x[i] = grad3[(perm[i] % 12)*3 + 0] + const permGrad3x = new Float64Array(512); + const permGrad3y = new Float64Array(512); + const permGrad3z = new Float64Array(512); + for (let i = 0; i < 512; i++) { + const gIndex = (perm[i] % 12) * 3; + permGrad3x[i] = grad3[gIndex + 0]; + permGrad3y[i] = grad3[gIndex + 1]; + permGrad3z[i] = grad3[gIndex + 2]; + } + + return function noise3DWithDerivatives(x: number, y: number, z: number, output?: NoiseDeriv3DOutput): NoiseDeriv3DOutput { + // Skew the input space + const s = (x + y + z) * F3; + const i = fastFloor(x + s); + const j = fastFloor(y + s); + const k = fastFloor(z + s); + + const t = (i + j + k) * G3; + // Unskew + const X0 = i - t; + const Y0 = j - t; + const Z0 = k - t; + + // Distances from cell origin + const x0 = x - X0; + const y0 = y - Y0; + const z0 = z - Z0; + + // Determine simplex region + let i1, j1, k1; + let i2, j2, k2; + + if (x0 >= y0) { + if (y0 >= z0) { + // X Y Z order + i1 = 1; + j1 = 0; + k1 = 0; + i2 = 1; + j2 = 1; + k2 = 0; + } else if (x0 >= z0) { + // X Z Y order + i1 = 1; + j1 = 0; + k1 = 0; + i2 = 1; + j2 = 0; + k2 = 1; + } else { + // Z X Y order + i1 = 0; + j1 = 0; + k1 = 1; + i2 = 1; + j2 = 0; + k2 = 1; + } + } else { + // x0 < y0 + if (y0 < z0) { + // Z Y X order + i1 = 0; + j1 = 0; + k1 = 1; + i2 = 0; + j2 = 1; + k2 = 1; + } else if (x0 < z0) { + // Y Z X order + i1 = 0; + j1 = 1; + k1 = 0; + i2 = 0; + j2 = 1; + k2 = 1; + } else { + // Y X Z order + i1 = 0; + j1 = 1; + k1 = 0; + i2 = 1; + j2 = 1; + k2 = 0; + } + } + + // Offsets for second corner + const x1 = x0 - i1 + G3; + const y1 = y0 - j1 + G3; + const z1 = z0 - k1 + G3; + // Offsets for third corner + const x2 = x0 - i2 + 2.0 * G3; + const y2 = y0 - j2 + 2.0 * G3; + const z2 = z0 - k2 + 2.0 * G3; + // Offsets for last corner + const x3 = x0 - 1.0 + 3.0 * G3; + const y3 = y0 - 1.0 + 3.0 * G3; + const z3 = z0 - 1.0 + 3.0 * G3; + + // Hashed gradient indices + const ii = i & 255; + const jj = j & 255; + const kk = k & 255; + + // Contribution accumulators + let n0 = 0, + n1 = 0, + n2 = 0, + n3 = 0; + let dx0 = 0, + dy0 = 0, + dz0 = 0; + let dx1 = 0, + dy1 = 0, + dz1 = 0; + let dx2 = 0, + dy2 = 0, + dz2 = 0; + let dx3 = 0, + dy3 = 0, + dz3 = 0; + + // For each corner, we compute t_i, the gradient dot, and the derivatives + + // Corner 0 + { + const t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; + if (t0 > 0) { + const gi0 = ii + perm[jj + perm[kk]]; + const gx0 = permGrad3x[gi0]; + const gy0 = permGrad3y[gi0]; + const gz0 = permGrad3z[gi0]; + + const gDot0 = gx0 * x0 + gy0 * y0 + gz0 * z0; + const t0Sq = t0 * t0; + const t0Pow4 = t0Sq * t0Sq; // t0^4 + + n0 = t0Pow4 * gDot0; + + // Derivative of (t0^4 * (g·(x,y,z))) w.r.t x: + // d/dx [t0^4 * gDot0] = (d/dx t0^4)*gDot0 + t0^4*gx0 + // where t0 = 0.6 - (x^2 + y^2 + z^2), + // d/dx t0^4 = 4 * t0^3 * d/dx t0 = 4*t0^3*(-2*x) = -8*t0^3*x + // => partial_x = -8*t0^3*x0*gDot0 + t0^4*gx0 + const t0Cub = t0Sq * t0; // t0^3 + const coeff = -8.0 * t0Cub; // factor for x*gDot + dx0 = coeff * x0 * gDot0 + t0Pow4 * gx0; + dy0 = coeff * y0 * gDot0 + t0Pow4 * gy0; + dz0 = coeff * z0 * gDot0 + t0Pow4 * gz0; + } + } + + // Corner 1 + { + const t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; + if (t1 > 0) { + const gi1 = ii + i1 + perm[jj + j1 + perm[kk + k1]]; + const gx1 = permGrad3x[gi1]; + const gy1 = permGrad3y[gi1]; + const gz1 = permGrad3z[gi1]; + + const gDot1 = gx1 * x1 + gy1 * y1 + gz1 * z1; + const t1Sq = t1 * t1; + const t1Pow4 = t1Sq * t1Sq; + + n1 = t1Pow4 * gDot1; + + const t1Cub = t1Sq * t1; + const coeff = -8.0 * t1Cub; + dx1 = coeff * x1 * gDot1 + t1Pow4 * gx1; + dy1 = coeff * y1 * gDot1 + t1Pow4 * gy1; + dz1 = coeff * z1 * gDot1 + t1Pow4 * gz1; + } + } + + // Corner 2 + { + const t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; + if (t2 > 0) { + const gi2 = ii + i2 + perm[jj + j2 + perm[kk + k2]]; + const gx2 = permGrad3x[gi2]; + const gy2 = permGrad3y[gi2]; + const gz2 = permGrad3z[gi2]; + + const gDot2 = gx2 * x2 + gy2 * y2 + gz2 * z2; + const t2Sq = t2 * t2; + const t2Pow4 = t2Sq * t2Sq; + + n2 = t2Pow4 * gDot2; + + const t2Cub = t2Sq * t2; + const coeff = -8.0 * t2Cub; + dx2 = coeff * x2 * gDot2 + t2Pow4 * gx2; + dy2 = coeff * y2 * gDot2 + t2Pow4 * gy2; + dz2 = coeff * z2 * gDot2 + t2Pow4 * gz2; + } + } + + // Corner 3 + { + const t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; + if (t3 > 0) { + const gi3 = ii + 1 + perm[jj + 1 + perm[kk + 1]]; + const gx3 = permGrad3x[gi3]; + const gy3 = permGrad3y[gi3]; + const gz3 = permGrad3z[gi3]; + + const gDot3 = gx3 * x3 + gy3 * y3 + gz3 * z3; + const t3Sq = t3 * t3; + const t3Pow4 = t3Sq * t3Sq; + + n3 = t3Pow4 * gDot3; + + const t3Cub = t3Sq * t3; + const coeff = -8.0 * t3Cub; + dx3 = coeff * x3 * gDot3 + t3Pow4 * gx3; + dy3 = coeff * y3 * gDot3 + t3Pow4 * gy3; + dz3 = coeff * z3 * gDot3 + t3Pow4 * gz3; + } + } + + // Sum up contributions + const value = n0 + n1 + n2 + n3; + const dx = dx0 + dx1 + dx2 + dx3; + const dy = dy0 + dy1 + dy2 + dy3; + const dz = dz0 + dz1 + dz2 + dz3; + + // The original 3D simplex scaling factor is 32.0 in this library + // (makes final output ~in [-1,1]). + const scale = 32.0; + + if (output) { + output.value = scale * value; + output.dx = scale * dx; + output.dy = scale * dy; + output.dz = scale * dz; + return output; + } + + return { + value: scale * value, + dx: scale * dx, + dy: scale * dy, + dz: scale * dz, + }; + }; +} + + +/** + * The output of the noise function with derivatives + * + * @property value the noise value in the interval [-1, 1] + * @property dx the partial derivative with respect to x + * @property dy the partial derivative with respect to y + * @property dz the partial derivative with respect to z + * @property dw the partial derivative with respect to w + */ +export type NoiseDeriv4DOutput = { + value: number; + dx: number; + dy: number; + dz: number; + dw: number; +}; + +/** + * Samples the noise field in four dimensions and returns the partial derivatives (dx, dy, dz, dw). + * + * Coordinates should be finite, bigger than -2^31 and smaller than 2^31. + * @param x + * @param y + * @param z + * @param w + * @param output (optional) an object to re-use for the result, so as to avoid allocations + * @returns {NoiseDeriv4DOutput} + */ +export type NoiseDerivFunction4D = ( + x: number, + y: number, + z: number, + w: number, + output?: NoiseDeriv4DOutput +) => NoiseDeriv4DOutput; + +/** + * Creates a 4D Simplex noise function that also computes its analytical partial derivatives. + * + * @param random the random function that will be used to build the permutation table + * @returns {NoiseDerivFunction4D} + */ +export function createNoise4DWithDerivatives(random: RandomFn = Math.random): NoiseDerivFunction4D { + // Build the standard permutation table + const perm = buildPermutationTable(random); + + // Precompute gradient indices for each possible value in `perm` (0..511). + // Because the default grad4 array has 32 different grad vectors for 4D, we do `perm[i] % 32`. + const permGrad4x = new Float64Array(512); + const permGrad4y = new Float64Array(512); + const permGrad4z = new Float64Array(512); + const permGrad4w = new Float64Array(512); + for (let i = 0; i < 512; i++) { + const gi = (perm[i] % 32) * 4; + permGrad4x[i] = grad4[gi + 0]; + permGrad4y[i] = grad4[gi + 1]; + permGrad4z[i] = grad4[gi + 2]; + permGrad4w[i] = grad4[gi + 3]; + } + + return function noise4DWithDerivatives( + x: number, + y: number, + z: number, + w: number, + output?: NoiseDeriv4DOutput + ): NoiseDeriv4DOutput { + // Skew the (x,y,z,w) space + const s = (x + y + z + w) * F4; + const i = fastFloor(x + s); + const j = fastFloor(y + s); + const k = fastFloor(z + s); + const l = fastFloor(w + s); + + // Unskew + const t = (i + j + k + l) * G4; + const X0 = i - t; + const Y0 = j - t; + const Z0 = k - t; + const W0 = l - t; + + // Distances from cell origin + const x0 = x - X0; + const y0 = y - Y0; + const z0 = z - Z0; + const w0 = w - W0; + + // 4D simplex region rank ordering + let rankx = 0; + let ranky = 0; + let rankz = 0; + let rankw = 0; + if (x0 > y0) rankx++; else ranky++; + if (x0 > z0) rankx++; else rankz++; + if (x0 > w0) rankx++; else rankw++; + if (y0 > z0) ranky++; else rankz++; + if (y0 > w0) ranky++; else rankw++; + if (z0 > w0) rankz++; else rankw++; + + // Offsets for each corner + const i1 = rankx >= 3 ? 1 : 0; + const j1 = ranky >= 3 ? 1 : 0; + const k1 = rankz >= 3 ? 1 : 0; + const l1 = rankw >= 3 ? 1 : 0; + + const i2 = rankx >= 2 ? 1 : 0; + const j2 = ranky >= 2 ? 1 : 0; + const k2 = rankz >= 2 ? 1 : 0; + const l2 = rankw >= 2 ? 1 : 0; + + const i3 = rankx >= 1 ? 1 : 0; + const j3 = ranky >= 1 ? 1 : 0; + const k3 = rankz >= 1 ? 1 : 0; + const l3 = rankw >= 1 ? 1 : 0; + + // Position deltas for each corner + const x1 = x0 - i1 + G4; + const y1 = y0 - j1 + G4; + const z1 = z0 - k1 + G4; + const w1 = w0 - l1 + G4; + + const x2 = x0 - i2 + 2.0 * G4; + const y2 = y0 - j2 + 2.0 * G4; + const z2 = z0 - k2 + 2.0 * G4; + const w2 = w0 - l2 + 2.0 * G4; + + const x3 = x0 - i3 + 3.0 * G4; + const y3 = y0 - j3 + 3.0 * G4; + const z3 = z0 - k3 + 3.0 * G4; + const w3 = w0 - l3 + 3.0 * G4; + + const x4 = x0 - 1.0 + 4.0 * G4; + const y4 = y0 - 1.0 + 4.0 * G4; + const z4 = z0 - 1.0 + 4.0 * G4; + const w4 = w0 - 1.0 + 4.0 * G4; + + // Hashed gradient indices + const ii = i & 255; + const jj = j & 255; + const kk = k & 255; + const ll = l & 255; + + // Corner accumulators + let n0 = 0, n1 = 0, n2 = 0, n3 = 0, n4 = 0; + let dx0 = 0, dx1 = 0, dx2 = 0, dx3 = 0, dx4 = 0; + let dy0 = 0, dy1 = 0, dy2 = 0, dy3 = 0, dy4 = 0; + let dz0 = 0, dz1 = 0, dz2 = 0, dz3 = 0, dz4 = 0; + let dw0 = 0, dw1 = 0, dw2 = 0, dw3 = 0, dw4 = 0; + + // A small helper to do the repeated derivative logic + function cornerContribution( + tx: number, ty: number, tz: number, tw: number, + cornerIdx: number + ) { + // t = 0.6 - sum of squares + const tVal = 0.6 - (tx * tx + ty * ty + tz * tz + tw * tw); + if (tVal <= 0) { + return { + n: 0, dx: 0, dy: 0, dz: 0, dw: 0 + }; + } + // Hashed gradient + const gx = permGrad4x[cornerIdx]; + const gy = permGrad4y[cornerIdx]; + const gz = permGrad4z[cornerIdx]; + const gw = permGrad4w[cornerIdx]; + + // Gradient dot + const gDot = gx * tx + gy * ty + gz * tz + gw * tw; + + // t^2, t^3, t^4 + const t2 = tVal * tVal; + const t4 = t2 * t2; // t^4 + const t3 = t2 * tVal; // t^3 + + // Contribution + const n = t4 * gDot; + + // Derivatives: + // dn/d(tx) = ∂/∂(tx) [ t^4 * (grad·(tx,ty,tz,tw)) ] + // = (4 * t^3 * d(t)/d(tx)) * gDot + t^4 * gx + // where d(t)/d(tx) = -2*tx + const factor = -8.0 * t3; // 4 * t^3 * -2 + const dx = factor * tx * gDot + t4 * gx; + const dy = factor * ty * gDot + t4 * gy; + const dz = factor * tz * gDot + t4 * gz; + const dw = factor * tw * gDot + t4 * gw; + + return { n, dx, dy, dz, dw }; + } + + // Evaluate each corner's contribution + { + // Corner 0 + const gi0 = ii + perm[jj + perm[kk + perm[ll]]]; + const c0 = cornerContribution(x0, y0, z0, w0, gi0); + n0 = c0.n; dx0 = c0.dx; dy0 = c0.dy; dz0 = c0.dz; dw0 = c0.dw; + } + { + // Corner 1 + const gi1 = ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]; + const c1 = cornerContribution(x1, y1, z1, w1, gi1); + n1 = c1.n; dx1 = c1.dx; dy1 = c1.dy; dz1 = c1.dz; dw1 = c1.dw; + } + { + // Corner 2 + const gi2 = ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]; + const c2 = cornerContribution(x2, y2, z2, w2, gi2); + n2 = c2.n; dx2 = c2.dx; dy2 = c2.dy; dz2 = c2.dz; dw2 = c2.dw; + } + { + // Corner 3 + const gi3 = ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]; + const c3 = cornerContribution(x3, y3, z3, w3, gi3); + n3 = c3.n; dx3 = c3.dx; dy3 = c3.dy; dz3 = c3.dz; dw3 = c3.dw; + } + { + // Corner 4 + const gi4 = ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]; + const c4 = cornerContribution(x4, y4, z4, w4, gi4); + n4 = c4.n; dx4 = c4.dx; dy4 = c4.dy; dz4 = c4.dz; dw4 = c4.dw; + } + + // Sum contributions + let value = n0 + n1 + n2 + n3 + n4; + let dx = dx0 + dx1 + dx2 + dx3 + dx4; + let dy = dy0 + dy1 + dy2 + dy3 + dy4; + let dz = dz0 + dz1 + dz2 + dz3 + dz4; + let dw = dw0 + dw1 + dw2 + dw3 + dw4; + + // Scale it to ~[-1, 1], consistent with createNoise4D + const scale = 27.0; + value *= scale; + dx *= scale; + dy *= scale; + dz *= scale; + dw *= scale; + + if (!output) { + return { value, dx, dy, dz, dw }; + } + // Re-use the output object + output.value = value; + output.dx = dx; + output.dy = dy; + output.dz = dz; + output.dw = dw; + return output; + }; +} + + diff --git a/snapshots/noise3DlargeWithDerivatives.png b/snapshots/noise3DlargeWithDerivatives.png new file mode 100644 index 0000000000000000000000000000000000000000..84e8b44313a664102b983dcae61b665e91216a5d GIT binary patch literal 16521 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 VaKFxBL3;oI002ovPDHLkV1lRboZ$cf literal 0 HcmV?d00001 diff --git a/snapshots/noise4DlargeWithDerivatives.png b/snapshots/noise4DlargeWithDerivatives.png new file mode 100644 index 0000000000000000000000000000000000000000..806ac2c4051ae2469482b3c67ec999f542d81547 GIT binary patch literal 16521 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 z f(x / width * 2 - 1, y / height * 2 - 1) * 128 + 127, width, height); +} + + +export function assertMatchesRGBAImage(actual: ImageDataLike, imageFilename: string): void { + if (!imageFilename.endsWith('.png')) { + console.log('throwing'); + throw new Error('imageFilename must end in .png'); + } + let fileBuffer; + try { + fileBuffer = fs.readFileSync(path.join(snapshotsPath, imageFilename)); + } + catch (_) { + writeRGBAImageSnapshot(actual, imageFilename); + return; + } + const png = PNG.sync.read(fileBuffer); + if (actual.data.length !== png.data.length) { + throw new Error('Expected actual.length to match png.data.length'); + } + const identical = actual.data.every((value, i) => value == png.data[i]); + if (!identical) { + console.log(png.data); + writeRGBAImageSnapshot(actual, imageFilename.replace('.png', '.error.png')); + throw new Error('expected data to be identitcal'); + } +} + +export function writeRGBAImageSnapshot(actual: ImageDataLike, imageFilename: string) { + const png = new PNG({ + colorType: 6, + inputColorType: 6, + bitDepth: 16, + width: actual.width, + height: actual.height, + inputHasAlpha: false, + }); + if (actual.data.length !== png.data.length) { + console.warn(actual.data.length, png.data.length); + throw new Error('Expected actual.data.length to match png.data.length'); + } + png.data.forEach((_, i, a) => a[i] = actual.data[(i) | 0]); + + fs.writeFileSync(path.join(snapshotsPath, imageFilename), PNG.sync.write(png.pack(), { colorType: 6 })); +} + +type RGBASampleFunction = (x: number, y: number) => [number, number, number, number]; +export function sampleFunctionToRGBAImageData(f: RGBASampleFunction, width: number, height: number): ImageDataLike { + const imageData = { + width, + height, + data: new Uint8ClampedArray(width * height * 4) + }; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const [r, g, b, a] = f(x, y); + imageData.data[(y * width + x) * 4] = r; + imageData.data[(y * width + x) * 4 + 1] = g; + imageData.data[(y * width + x) * 4 + 2] = b; + imageData.data[(y * width + x) * 4 + 3] = a; + } + } + return imageData; +} + +// same as sampleFunctionToImageData but x and y go from -1 .. 1 instead of 0 .. width +// output is not scaled, scale that yourself to be between 0 and 255 for each channel +export function sampleFunctionToRGBAImageDataOne(f: RGBASampleFunction, width: number, height: number): ImageDataLike { + return sampleFunctionToRGBAImageData((x, y) => f(x / width * 2 - 1, y / height * 2 - 1), width, height); } \ No newline at end of file diff --git a/test/simplex-noise-test.ts b/test/simplex-noise-test.ts index f759cc9..9cc8314 100644 --- a/test/simplex-noise-test.ts +++ b/test/simplex-noise-test.ts @@ -1,10 +1,9 @@ -import { createNoise2D, createNoise3D, createNoise4D } from '../simplex-noise'; -import { buildPermutationTable } from '../simplex-noise'; +import { createNoise2D, createNoise2DWithDerivatives, createNoise3D, createNoise3DWithDerivatives, createNoise4D, buildPermutationTable, createNoise4DWithDerivatives } from '../simplex-noise'; import alea from 'alea'; import { assert } from 'chai'; -import { assertMatchesImage, sampleFunctionToImageDataOne } from './matches-snapshot'; +import { assertMatchesImage, assertMatchesRGBAImage, sampleFunctionToImageDataOne, sampleFunctionToRGBAImageDataOne } from './matches-snapshot'; function getRandom(seed = 'seed') { return alea(seed); @@ -200,3 +199,278 @@ describe('createNoise4D', () => { }); }); }); + + + +describe('createNoise2DWithDerivatives', () => { + const noise2DWithDerivatives = createNoise2DWithDerivatives(getRandom()); + const noise2D = createNoise2D(getRandom()); + + describe('noise2DWithDerivatives', () => { + it('is initialized randomly without arguments', function () { + const noise3DA = createNoise3DWithDerivatives(); + const noise3DB = createNoise3DWithDerivatives(); + assert.notEqual(noise3DA(0.1, 0.1, 0.1), noise3DB(0.0, 0.1, 0.1)); + }); + it('should return the same value for the same input', function () { + assert.equal(noise2DWithDerivatives(0.1, 0.2).value, noise2D(0.1, 0.2)); + }); + it('should return a different value for a different input', function () { + assert.notEqual(noise2DWithDerivatives(0.1, 0.2).value, noise2DWithDerivatives(0.101, 0.202).value); + }); + it('should return a different output with a different seed', function () { + const noise2D2 = createNoise2DWithDerivatives(getRandom('other seed')); + assert.notEqual(noise2DWithDerivatives(0.1, 0.2).value, noise2D2(0.1, 0.2).value); + }); + it('should return values between -1 and 1', function () { + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + assert(noise2DWithDerivatives(x / 5, y / 5).value >= -1); + assert(noise2DWithDerivatives(x / 5, y / 5).value <= 1); + } + } + }); + it('should return similar values for similar inputs', function () { + assert(Math.abs(noise2DWithDerivatives(0.1, 0.2).value - noise2DWithDerivatives(0.101, 0.202).value) < 0.1); + }); + it('should match snapshot for small inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise2DWithDerivatives(x * 2, y * 2).value, size, size); + assertMatchesImage(actual, 'noise2Dsmall.png'); + }); + it('should match snapshot for large inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise2DWithDerivatives(x * 1000, y * 1000).value, size, size); + assertMatchesImage(actual, 'noise2Dlarge.png'); + }); + it('should be close to finite difference derivatives', function () { + const epsilon = 0.00001; + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + const output = noise2DWithDerivatives(x / 5, y / 5); + + const dx = (noise2D(x / 5 + epsilon, y / 5) - output.value) / epsilon; + const dy = (noise2D(x / 5, y / 5 + epsilon) - output.value) / epsilon; + + assert(Math.abs(dx - output.dx) < 0.001); + assert(Math.abs(dy - output.dy) < 0.001); + } + } + }); + it('should use supplied output parameter if provided', function () { + const output = { value: 0, dx: 0, dy: 0 }; + const newOutput = noise2DWithDerivatives(0.1, 0.2, output); + assert.equal(output.value, noise2D(0.1, 0.2)); + assert.equal(newOutput, output); + }); + }); +}); + +describe('createNoise3DWithDerivatives', () => { + const noise3DWithDerivatives = createNoise3DWithDerivatives(getRandom()); + const noise3D = createNoise3D(getRandom()); + + describe('noise3DWithDerivatives', () => { + it('is initialized randomly without arguments', function () { + const noise3DA = createNoise3DWithDerivatives(); + const noise3DB = createNoise3DWithDerivatives(); + assert.notEqual(noise3DA(0.1, 0.1, 0.1), noise3DB(0.0, 0.1, 0.1)); + }); + it('should return the same value for the same input', function () { + assert.equal(noise3DWithDerivatives(0.1, 0.2, 0.3).value, noise3D(0.1, 0.2, 0.3)); + }); + it('should return a different value for a different input', function () { + assert.notEqual(noise3DWithDerivatives(0.1, 0.2, 0.3).value, noise3DWithDerivatives(0.101, 0.202, 0.303).value); + }); + it('should return a different output with a different seed', function () { + const noise3D2 = createNoise3DWithDerivatives(getRandom('other seed')); + assert.notEqual(noise3DWithDerivatives(0.1, 0.2, 0.3).value, noise3D2(0.1, 0.2, 0.3).value); + }); + it('should return values between -1 and 1', function () { + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + assert(noise3DWithDerivatives(x / 5, y / 5, x + y).value >= -1); + assert(noise3DWithDerivatives(x / 5, y / 5, x + y).value <= 1); + } + } + }); + it('should return similar values for similar inputs', function () { + assert(Math.abs(noise3DWithDerivatives(0.1, 0.2, 0.3).value - noise3DWithDerivatives(0.101, 0.202, 0.303).value) < 0.1); + }); + it('should match snapshot for small inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise3DWithDerivatives(x * 2, y * 2, (x + y)).value, size, size); + assertMatchesImage(actual, 'noise3Dsmall.png'); + }); + it('should match snapshot for large inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise3DWithDerivatives(x * 1000, y * 1000, (x + y) * 500).value, size, size); + assertMatchesImage(actual, 'noise3Dlarge.png'); + }); + it('should be close to finite difference derivatives', function () { + const epsilon = 0.00001; + for (let x = 0; x < 20; x++) { + for (let y = 0; y < 20; y++) { + // this test can fail on boundaries, so we have to use smaller inputs + const [ix, iy, iz] = [x / 50, y / 50, (x + y) / 80]; + const output = noise3DWithDerivatives(ix, iy, iz); + + const dx = (noise3D(ix + epsilon, iy, iz) - output.value) / epsilon; + const dy = (noise3D(ix, iy + epsilon, iz) - output.value) / epsilon; + const dz = (noise3D(ix, iy, iz + epsilon) - output.value) / epsilon; + + assert(Math.abs(dx - output.dx) < 0.001); + assert(Math.abs(dy - output.dy) < 0.001); + assert(Math.abs(dz - output.dz) < 0.001); + } + } + }); + it('should use supplied output parameter if provided', function () { + const output = { value: 0, dx: 0, dy: 0, dz: 0 }; + const newOutput = noise3DWithDerivatives(0.1, 0.2, 0.3, output); + assert.equal(output.value, noise3D(0.1, 0.2, 0.3)); + assert.equal(newOutput, output); + }); + it('should match small snapshot with derivatives', function () { + const size = 64; + const actual = sampleFunctionToRGBAImageDataOne((x, y) => { + const output = noise3DWithDerivatives(x * 2, y * 2, (x + y)); + + // cap dx, dy, dz to be max length 1 + const length = Math.sqrt(output.dx * output.dx + output.dy * output.dy + output.dz * output.dz); + if(length > 1) { + output.dx /= length; + output.dy /= length; + output.dz /= length; + } + + return [(output.dx + 1) * 128, (output.dy + 1) * 128, (output.dz + 1) * 128, output.value * 128 + 128]; + }, size, size); + assertMatchesRGBAImage(actual, 'noise3DsmallWithDerivatives.png'); + }); + it('should match large snapshot with derivatives', function () { + const size = 64; + const actual = sampleFunctionToRGBAImageDataOne((x, y) => { + const output = noise3DWithDerivatives(x * 1000, y * 1000, (x + y) * 500); + + // cap dx, dy, dz to be max length 1 + const length = Math.sqrt(output.dx * output.dx + output.dy * output.dy + output.dz * output.dz); + if(length > 1) { + output.dx /= length; + output.dy /= length; + output.dz /= length; + } + + return [(output.dx + 1) * 128, (output.dy + 1) * 128, (output.dz + 1) * 128, output.value * 128 + 128]; + }, size, size); + assertMatchesRGBAImage(actual, 'noise3DlargeWithDerivatives.png'); + }); + }); +}); + + +describe('createNoise4DWithDerivatives', () => { + const noise4DWithDerivatives = createNoise4DWithDerivatives(getRandom()); + const noise4D = createNoise4D(getRandom()); + + describe('noise4DWithDerivatives', () => { + it('is initialized randomly without arguments', function () { + const noise4DA = createNoise4DWithDerivatives(); + const noise4DB = createNoise4DWithDerivatives(); + assert.notEqual(noise4DA(0.1, 0.1, 0.1, 0.1), noise4DB(0.0, 0.1, 0.1, 0.1)); + }); + it('should return the same value for the same input', function () { + assert.equal(noise4DWithDerivatives(0.1, 0.2, 0.3, 0.4).value, noise4D(0.1, 0.2, 0.3, 0.4)); + }); + it('should return a different value for a different input', function () { + assert.notEqual(noise4DWithDerivatives(0.1, 0.2, 0.3, 0.4).value, noise4DWithDerivatives(0.101, 0.202, 0.303, 0.404).value); + }); + it('should return a different output with a different seed', function () { + const noise4D2 = createNoise4DWithDerivatives(getRandom('other seed')); + assert.notEqual(noise4DWithDerivatives(0.1, 0.2, 0.3, 0.4).value, noise4D2(0.1, 0.2, 0.3, 0.4).value); + }); + it('should return values between -1 and 1', function () { + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + assert(noise4DWithDerivatives(x / 5, y / 5, x + y, x - y).value >= -1); + assert(noise4DWithDerivatives(x / 5, y / 5, x + y, x - y).value <= 1); + } + } + }); + it('should return similar values for similar inputs', function () { + assert(Math.abs(noise4DWithDerivatives(0.1, 0.2, 0.3, 0.4).value - noise4DWithDerivatives(0.101, 0.202, 0.303, 0.404).value) < 0.1); + }); + it('should match snapshot for small inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise4DWithDerivatives(x * 2, y * 2, (x + y), (x - y)).value, size, size); + assertMatchesImage(actual, 'noise4Dsmall.png'); + }); + it('should match snapshot for large inputs', function () { + const size = 64; + const actual = sampleFunctionToImageDataOne((x, y) => noise4DWithDerivatives(x * 1000, y * 1000, (x + y) * 500, (x - y) * 500).value, size, size); + assertMatchesImage(actual, 'noise4Dlarge.png'); + }); + it('should be close to finite difference derivatives', function () { + const epsilon = 0.00001; + for (let x = 0; x < 20; x++) { + for (let y = 0; y < 20; y++) { + // this test can fail on boundaries, so we have to use smaller inputs + const [ix, iy, iz, iw] = [x / 50, y / 50, (x + y) / 80, (x - y) / 80]; + const output = noise4DWithDerivatives(ix, iy, iz, iw); + + const dx = (noise4D(ix + epsilon, iy, iz, iw) - output.value) / epsilon; + const dy = (noise4D(ix, iy + epsilon, iz, iw) - output.value) / epsilon; + const dz = (noise4D(ix, iy, iz + epsilon, iw) - output.value) / epsilon; + const dw = (noise4D(ix, iy, iz, iw + epsilon) - output.value) / epsilon; + + assert(Math.abs(dx - output.dx) < 0.001); + assert(Math.abs(dy - output.dy) < 0.001); + assert(Math.abs(dz - output.dz) < 0.001); + assert(Math.abs(dw - output.dw) < 0.001); + } + } + }); + it('should use supplied output parameter if provided', function () { + const output = { value: 0, dx: 0, dy: 0, dz: 0, dw: 0 }; + const newOutput = noise4DWithDerivatives(0.1, 0.2, 0.3, 0.4, output); + assert.equal(output.value, noise4D(0.1, 0.2, 0.3, 0.4)); + assert.equal(newOutput, output); + }); + it('should match small snapshot with derivatives', function () { + const size = 64; + const actual = sampleFunctionToRGBAImageDataOne((x, y) => { + const output = noise4DWithDerivatives(x * 2, y * 2, (x + y), (x - y)); + + // cap dx, dy, dz, dw to be max length 1 + const length = Math.sqrt(output.dx * output.dx + output.dy * output.dy + output.dz * output.dz + output.dw * output.dw); + if(length > 1) { + output.dx /= length; + output.dy /= length; + output.dz /= length; + output.dw /= length; + } + + return [(output.dx + 1) * 128, (output.dy + 1) * 128, (output.dz + 1) * 128, (output.dw + 1) * 128]; + }, size, size); + assertMatchesRGBAImage(actual, 'noise4DsmallWithDerivatives.png'); + }); + it('should match large snapshot with derivatives', function () { + const size = 64; + const actual = sampleFunctionToRGBAImageDataOne((x, y) => { + const output = noise4DWithDerivatives(x * 1000, y * 1000, (x + y) * 500, (x - y) * 500); + + // cap dx, dy, dz, dw to be max length 1 + const length = Math.sqrt(output.dx * output.dx + output.dy * output.dy + output.dz * output.dz + output.dw * output.dw); + if(length > 1) { + output.dx /= length; + output.dy /= length; + output.dz /= length; + output.dw /= length; + } + + return [(output.dx + 1) * 128, (output.dy + 1) * 128, (output.dz + 1) * 128, (output.dw + 1) * 128]; + }, size, size); + assertMatchesRGBAImage(actual, 'noise4DlargeWithDerivatives.png'); + }); + }); +}); \ No newline at end of file From 416f6d8b2aa1c8b0eb5e16658734d03d33da3319 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:59:50 +0100 Subject: [PATCH 3/3] fix doc string --- simplex-noise.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/simplex-noise.ts b/simplex-noise.ts index 255a0ca..b0c6ab5 100644 --- a/simplex-noise.ts +++ b/simplex-noise.ts @@ -525,12 +525,8 @@ export type NoiseDeriv2DOutput = { export type NoiseDerivFunction2D = (x: number, y: number, output?: NoiseDeriv2DOutput) => NoiseDeriv2DOutput; /** - * Creates a 2D simplex noise function that also returns the analytical derivatives: - * value = noise(x, y) - * dx = ∂(noise)/∂x - * dy = ∂(noise)/∂y - * - * Returns a function that takes (x, y) and returns { value, dx, dy }. + * Creates a 2D noise function with derivatives + * * @param random the random function that will be used to build the permutation table * @returns {NoiseDerivFunction2D} */