diff --git a/simplex-noise.ts b/simplex-noise.ts index 188e62a..b0c6ab5 100644 --- a/simplex-noise.ts +++ b/simplex-noise.ts @@ -497,4 +497,728 @@ 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 noise function with derivatives + * + * @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 0000000..84e8b44 Binary files /dev/null and b/snapshots/noise3DlargeWithDerivatives.png differ diff --git a/snapshots/noise3DsmallWithDerivatives.png b/snapshots/noise3DsmallWithDerivatives.png new file mode 100644 index 0000000..1ca308a Binary files /dev/null and b/snapshots/noise3DsmallWithDerivatives.png differ diff --git a/snapshots/noise4DlargeWithDerivatives.png b/snapshots/noise4DlargeWithDerivatives.png new file mode 100644 index 0000000..806ac2c Binary files /dev/null and b/snapshots/noise4DlargeWithDerivatives.png differ diff --git a/snapshots/noise4DsmallWithDerivatives.png b/snapshots/noise4DsmallWithDerivatives.png new file mode 100644 index 0000000..209168e Binary files /dev/null and b/snapshots/noise4DsmallWithDerivatives.png differ diff --git a/test/matches-snapshot.ts b/test/matches-snapshot.ts index 1543fdd..4627a29 100644 --- a/test/matches-snapshot.ts +++ b/test/matches-snapshot.ts @@ -74,4 +74,73 @@ export function sampleFunctionToImageData(f: SampleFunction, width: number, heig // output is scaled from -1 .. 1 to 0 .. 255 export function sampleFunctionToImageDataOne(f: SampleFunction, width: number, height: number): ImageDataLike { return sampleFunctionToImageData((x, y) => 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