Skip to content

VolumetricNoise.kt

src/jvmMain/kotlin/afield/lang/stacksl/VolumetricNoise.kt · 736 lines · cited by GPU State — keeping a simulation on the card

package afield.lang.stacksl

import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.sqrt

StackSL's volumetric noise family: noise3, fbm3, fbmd3, warp3, curl3, worley3, noise4, fbm4 — eight words that work as true 3D and 4D fields.

Why a separate family

StackSL's original noise and fbm are two-dimensional. The word lowers to _noise(input.xy + input.zw * 0.07), so a vec4 carrying a position and a time is squashed into a plane: z enters at one fourteenth the weight of x, as a shear rather than as a third dimension. On a volume seen down z that reads as one fixed pattern imprinted at every depth. The two-slice-sum workaround (f(x,y) + f(y,z) + f(z,x)) is what people write instead, and it costs three fbms and does not give a 3D field — it gives three sheared 2D ones.

Three properties the 2D words lack

The range really is [0,1). _fbm(vec2) sums five octaves at amplitudes 0.5 0.25 0.125 0.0625 0.03125, which total 0.96875, not 1 — so the usual 0.5 - recentering leaves a constant bias of about -0.031. A constant force integrates into a drift instead of averaging out. Here the amplitude sum is accumulated at runtime and divided out, so the range is exactly [0,1) for any octave count, lacunarity and gain.

The hashes contain no sin(). _hash11(float) uses fract(sin(p * 127.1) * 43758.5453123), and sin is not bit-portable: its precision is implementation-defined, it loses all meaning for large arguments, and no CPU implementation can be trusted to agree with a GPU's. _hash21(vec2) — the hash that actually feeds _noise and _fbm — has no sin at all; it is fract(p * vec2(123.34, 456.21)), p += dot(p, p + 45.32), fract(p.x * p.y), so it is already multiply/add/fract and already IEEE-754 exact. (Exact is not the same as stable: fract of a large product amplifies a last-bit difference in its argument to O(1), which is why an exactly-specified noise can still disagree visibly across backends.) The reason this family could not use _hash21 is dimensionality: _hash21 takes a vec2, and a 3D/4D family needs a hash over 3 and 4 inputs. The hashes below are multiply/add/fract only, which makes VolumetricNoiseReference a real reference rather than an approximation. StackSLCpuVolumetricCrossCheckTest evaluates the emitted GLSL against every function here, bit-for-bit.

curl3 is divergence-free by construction. It is the curl of a vector potential taken with central differences at one shared epsilon. Discrete shift operators commute, so the discrete divergence of the discrete curl cancels term for term and is zero up to float rounding — see VolumetricNoiseTest. Particles advected through it swirl instead of piling into sinks.

Constants

VolumetricNoiseConstants is the single source of truth for the family's shared constants — the octave defaults, lacunarity, gain, the finite-difference epsilons, the hash multipliers and the per-octave rotation. The Kotlin reference reads them and the emitted GLSL interpolates them, so the two cannot drift apart. VolumetricNoiseTest's every shared constant appears in the emitted GLSL fails if one is ever typed into the shader text by hand. The algebraic coefficients inside VolumetricNoiseReference — the mix weights, the smoothstep polynomial — are spelled out at their call sites instead, because they are the definition of the operation rather than a tunable value.

data class NoiseVec3(val x: Float, val y: Float, val z: Float)

A float triple. Float rather than Double, because the GLSL this mirrors works in float.

operator fun plus(o: NoiseVec3) = NoiseVec3(x + o.x, y + o.y, z + o.z)

operator fun minus(o: NoiseVec3) = NoiseVec3(x - o.x, y - o.y, z - o.z)

operator fun times(s: Float) = NoiseVec3(x * s, y * s, z * s)

val length: Float get() = sqrt(x * x + y * y + z * z)

data class NoiseVec4(val x: Float, val y: Float, val z: Float, val w: Float)

A float quadruple, for the 4D words and for the packed worley3 result.

object VolumetricNoiseConstants

object VolumetricNoiseConstants {

const val MAX_OCTAVES = 12

Loop bound in the emitted GLSL. An octave request above this is clamped, not an error.

const val DEFAULT_OCTAVES = 5.0f

const val DEFAULT_CURL_OCTAVES = 3.0f

curl3 defaults to FEWER octaves than everything else, and the reason is visible the moment you render it.

A curl is a derivative, and octave i of an fbm contributes amplitude * frequency, which is (gain * lacunarity)^i, to a derivative. At the default gain 0.5 and lacunarity 2 that product is exactly 1, so EVERY octave contributes equally to the curl — the fractal sum is pink in the value but flat in its derivative. Adding octaves therefore adds high-frequency confetti rather than richness: measured, the mean magnitude is 0.71 at one octave and 0.74 at five, essentially unchanged, while the picture goes from smooth swirls to per-pixel noise. Three is where the structure is still legible.

fbmd3 keeps the full DEFAULT_OCTAVES because a surface normal wants the fine detail.

const val DEFAULT_LACUNARITY = 2.0f

const val DEFAULT_GAIN = 0.5f

const val DEFAULT_CURL_EPSILON = 0.05f

curl3's finite-difference step, in the same units as the input coordinate. Large enough that (f(p+e) - f(p-e)) / 2e is not dominated by float cancellation, small enough that the stencil still resolves the finest octave. At the default lacunarity the fifth octave has a feature size around 1/16, so 0.05 straddles roughly a third of it.

const val DEFAULT_GRADIENT_EPSILON = 0.02f

fbmd3's gradient step. Same trade-off, one field rather than three.

const val DEFAULT_WARP_STRENGTH = 0.6f

warp3's displacement strength, in input units.

const val HASH_SEED = 0.1731f

Added before every hash so the origin is not a fixed point. Without it hash31(0,0,0) is exactly 0 — fract(0 * 0.1031) is 0, the dot product of zero with anything is 0, and fract((0 + 0) * 0) is 0 — which puts a black dot on the origin of every field. The offset is non-integral, so no lattice corner can ever land on the new fixed point at -HASH_SEED.

const val HASH3_SCALE_X = 0.1031f

Hoskins-style hash multipliers. Nearly-but-not-quite equal, and none a ratio of the others.

const val HASH3_SCALE_Y = 0.1030f

const val HASH3_SCALE_Z = 0.0973f

const val HASH4_SCALE_W = 0.1099f

const val HASH31_ADD = 31.32f

const val HASH33_ADD = 33.33f

const val HASH41_ADD = 33.33f

val OCTAVE_ROTATION_COLUMNS: Array<NoiseVec3>

Per-octave rotation. Orthonormal (asserted in the tests), so it changes the direction of the lattice without changing the scale — the lacunarity does all the scaling. Without a rotation, every octave's lattice lines up with the last and the sum shows axis-aligned grid streaks. Stored as COLUMNS, which is what GLSL's mat3(...) constructor takes.

    val OCTAVE_ROTATION_COLUMNS: Array<NoiseVec3> = arrayOf(
        NoiseVec3(0.00f, -0.80f, -0.60f),
        NoiseVec3(0.80f, 0.36f, -0.48f),
        NoiseVec3(0.60f, -0.48f, 0.64f)
    )

val OCTAVE_OFFSET = NoiseVec3(17.17f, 9.53f, -23.71f)

Translation applied after each octave's rotate-and-scale, to decorrelate the lattices.

const val OCTAVE_OFFSET_W = 11.31f

val FIELD_OFFSET_A = NoiseVec3(0.0f, 0.0f, 0.0f)

The three offsets that turn one scalar fbm into a vector potential for curl3, and that separate warp3's three displacement fields. Far apart and non-integral, so the three components are decorrelated samples of the same field rather than three views of one blob.

val FIELD_OFFSET_B = NoiseVec3(31.416f, 17.234f, -9.871f)

val FIELD_OFFSET_C = NoiseVec3(-63.219f, 45.128f, 71.653f)

fun allScalars(): Map<String, Float>

Every float literal that must appear in the emitted GLSL, for the anti-drift test.

    fun allScalars(): Map<String, Float> = linkedMapOf(
        "HASH_SEED" to HASH_SEED,
        "HASH3_SCALE_X" to HASH3_SCALE_X,
        "HASH3_SCALE_Y" to HASH3_SCALE_Y,
        "HASH3_SCALE_Z" to HASH3_SCALE_Z,
        "HASH4_SCALE_W" to HASH4_SCALE_W,
        "HASH31_ADD" to HASH31_ADD,
        "HASH33_ADD" to HASH33_ADD,
        "OCTAVE_OFFSET_W" to OCTAVE_OFFSET_W,
        "DEFAULT_CURL_EPSILON" to DEFAULT_CURL_EPSILON,
        "DEFAULT_GRADIENT_EPSILON" to DEFAULT_GRADIENT_EPSILON,
        "DEFAULT_WARP_STRENGTH" to DEFAULT_WARP_STRENGTH
    )
}

object VolumetricNoiseReference

The CPU twin of the GLSL emitted by volumetricNoiseHelperSource. Every function here is a statement-for-statement transliteration of its shader counterpart, in Float, using only operations GLSL and the JVM both round identically. It exists so the family's semantics — ranges, continuity, worley3's F1 <= F2, curl3's divergence — can be asserted without a device, which is the only kind of test StackSL has ever been able to have.

It is a SEMANTIC reference, not a bit-exact GPU emulator: a driver is free to contract a multiply-add into an FMA and change the last bit or two. Nothing here depends on the last bit.

object VolumetricNoiseReference {
    private val K = VolumetricNoiseConstants

    private fun fract(v: Float): Float = v - floor(v)
    private fun fract(v: NoiseVec3) = NoiseVec3(fract(v.x), fract(v.y), fract(v.z))

GLSL's mix, in GLSL's own algebra: x * (1 - a) + y * a.

The obvious rewrite a + (b - a) * t is algebraically identical and a DIFFERENT floating-point expression, and it is what this function used to be. That one line was the entire residual between this reference and the emitted shader it claims to mirror: measured over 4000 samples, noise3 matched only 2054 of them, off by up to 1.8e-7, while hash31, hash33 and hash41 — which contain no mix — were already bit-exact 20000 times out of 20000. curl3, which reaches noise3 through potential3, matched 5 times in 300.

With the spec's form every function in this file is bit-exact against the GLSL that StackSL.kt emits, evaluated by afield.lang.stacksl.cpu — see StackSLCpuVolumetricCrossCheckTest, which also contains the experiment that located this: noise3 reimplemented twice, differing only in how mix is spelled, each version matching one of the two sides at 4000/4000.

Every call site here mirrors a literal mix(...) in the emitted text (helperSource's noise3 and noise4_slice blocks), so the spec's form is correct at all of them and there is no site where the rewrite would have been the faithful reading.

    private fun mix(a: Float, b: Float, t: Float): Float = a * (1.0f - t) + b * t
    private fun smooth(f: Float): Float = f * f * (3.0f - 2.0f * f)

fun hash31(px: Float, py: Float, pz: Float): Float

vec3 -> [0,1). Multiply/add/fract only; see the header on why that matters.

    fun hash31(px: Float, py: Float, pz: Float): Float {
        var x = fract((px + K.HASH_SEED) * K.HASH3_SCALE_X)
        var y = fract((py + K.HASH_SEED) * K.HASH3_SCALE_Y)
        var z = fract((pz + K.HASH_SEED) * K.HASH3_SCALE_Z)
        val d = x * (z + K.HASH31_ADD) + y * (y + K.HASH31_ADD) + z * (x + K.HASH31_ADD)
        x += d
        y += d
        z += d
        return fract((x + y) * z)
    }

fun hash33(px: Float, py: Float, pz: Float): NoiseVec3

vec3 -> three independent values in [0,1), for worley3's feature-point jitter.

    fun hash33(px: Float, py: Float, pz: Float): NoiseVec3 {
        var x = fract((px + K.HASH_SEED) * K.HASH3_SCALE_X)
        var y = fract((py + K.HASH_SEED) * K.HASH3_SCALE_Y)
        var z = fract((pz + K.HASH_SEED) * K.HASH3_SCALE_Z)
        val d = x * (y + K.HASH33_ADD) + y * (x + K.HASH33_ADD) + z * (z + K.HASH33_ADD)
        x += d
        y += d
        z += d
        return NoiseVec3(fract((x + y) * z), fract((x + x) * y), fract((y + x) * x))
    }

fun hash41(px: Float, py: Float, pz: Float, pw: Float): Float

vec4 -> [0,1).

    fun hash41(px: Float, py: Float, pz: Float, pw: Float): Float {
        var x = fract((px + K.HASH_SEED) * K.HASH3_SCALE_X)
        var y = fract((py + K.HASH_SEED) * K.HASH3_SCALE_Y)
        var z = fract((pz + K.HASH_SEED) * K.HASH3_SCALE_Z)
        var w = fract((pw + K.HASH_SEED) * K.HASH4_SCALE_W)
        val d = x * (w + K.HASH41_ADD) + y * (z + K.HASH41_ADD) +
            z * (x + K.HASH41_ADD) + w * (y + K.HASH41_ADD)
        x += d
        y += d
        z += d
        w += d
        return fract((x + y) * (z + w))
    }

fun noise3(p: NoiseVec3): Float

Trilinearly interpolated value noise on the integer lattice, smoothstep-faded. The result is a convex combination of eight hashes each in [0,1), so it is in [0,1) exactly — no scaling, no bias, nothing to get wrong.

    fun noise3(p: NoiseVec3): Float {
        val ix = floor(p.x)
        val iy = floor(p.y)
        val iz = floor(p.z)
        val ux = smooth(p.x - ix)
        val uy = smooth(p.y - iy)
        val uz = smooth(p.z - iz)
        val c000 = hash31(ix, iy, iz)
        val c100 = hash31(ix + 1f, iy, iz)
        val c010 = hash31(ix, iy + 1f, iz)
        val c110 = hash31(ix + 1f, iy + 1f, iz)
        val c001 = hash31(ix, iy, iz + 1f)
        val c101 = hash31(ix + 1f, iy, iz + 1f)
        val c011 = hash31(ix, iy + 1f, iz + 1f)
        val c111 = hash31(ix + 1f, iy + 1f, iz + 1f)
        val z0 = mix(mix(c000, c100, ux), mix(c010, c110, ux), uy)
        val z1 = mix(mix(c001, c101, ux), mix(c011, c111, ux), uy)
        return mix(z0, z1, uz)
    }

fun noise4(p: NoiseVec4): Float

The 4D lattice, 16 corners. w is the animation axis when you want a volume that moves.

    fun noise4(p: NoiseVec4): Float {
        val ix = floor(p.x)
        val iy = floor(p.y)
        val iz = floor(p.z)
        val iw = floor(p.w)
        val ux = smooth(p.x - ix)
        val uy = smooth(p.y - iy)
        val uz = smooth(p.z - iz)
        val uw = smooth(p.w - iw)
        fun slice(dw: Float): Float {
            val c000 = hash41(ix, iy, iz, iw + dw)
            val c100 = hash41(ix + 1f, iy, iz, iw + dw)
            val c010 = hash41(ix, iy + 1f, iz, iw + dw)
            val c110 = hash41(ix + 1f, iy + 1f, iz, iw + dw)
            val c001 = hash41(ix, iy, iz + 1f, iw + dw)
            val c101 = hash41(ix + 1f, iy, iz + 1f, iw + dw)
            val c011 = hash41(ix, iy + 1f, iz + 1f, iw + dw)
            val c111 = hash41(ix + 1f, iy + 1f, iz + 1f, iw + dw)
            val z0 = mix(mix(c000, c100, ux), mix(c010, c110, ux), uy)
            val z1 = mix(mix(c001, c101, ux), mix(c011, c111, ux), uy)
            return mix(z0, z1, uz)
        }
        return mix(slice(0f), slice(1f), uw)
    }

fun rotateOctave(p: NoiseVec3): NoiseVec3

OCTAVE_ROTATION_COLUMNS applied as GLSL applies a mat3: sum of columns times components.

    fun rotateOctave(p: NoiseVec3): NoiseVec3 {
        val c = K.OCTAVE_ROTATION_COLUMNS
        return NoiseVec3(
            c[0].x * p.x + c[1].x * p.y + c[2].x * p.z,
            c[0].y * p.x + c[1].y * p.y + c[2].y * p.z,
            c[0].z * p.x + c[1].z * p.y + c[2].z * p.z
        )
    }

fun fbm3

Fractal sum. The amplitude sum is accumulated into norm and divided out at the end, so the range is exactly [0,1) whatever the octave count, lacunarity or gain — this is the structural fix for the 2D fbm's 0.96875 ceiling.

    fun fbm3(
        p: NoiseVec3,
        octaves: Float = K.DEFAULT_OCTAVES,
        lacunarity: Float = K.DEFAULT_LACUNARITY,
        gain: Float = K.DEFAULT_GAIN
    ): Float {
        val n = octaveCount(octaves)
        var q = p
        var amp = 1.0f
        var sum = 0.0f
        var norm = 0.0f
        for (i in 0 until n) {
            sum += amp * noise3(q)
            norm += amp
            q = rotateOctave(q) * lacunarity + K.OCTAVE_OFFSET
            amp *= gain
        }
        return if (norm > 0.0f) sum / norm else 0.0f
    }

fun fbm4

    fun fbm4(
        p: NoiseVec4,
        octaves: Float = K.DEFAULT_OCTAVES,
        lacunarity: Float = K.DEFAULT_LACUNARITY,
        gain: Float = K.DEFAULT_GAIN
    ): Float {
        val n = octaveCount(octaves)
        var q = p
        var amp = 1.0f
        var sum = 0.0f
        var norm = 0.0f
        for (i in 0 until n) {
            sum += amp * noise4(q)
            norm += amp
            val r = rotateOctave(NoiseVec3(q.x, q.y, q.z)) * lacunarity
            q = NoiseVec4(
                r.x + K.OCTAVE_OFFSET.x,
                r.y + K.OCTAVE_OFFSET.y,
                r.z + K.OCTAVE_OFFSET.z,
                q.w * lacunarity + K.OCTAVE_OFFSET_W
            )
            amp *= gain
        }
        return if (norm > 0.0f) sum / norm else 0.0f
    }

fun octaveCount(octaves: Float): Int

GLSL clamps then truncates; mirror that exactly rather than rounding.

    fun octaveCount(octaves: Float): Int =
        octaves.coerceIn(1.0f, K.MAX_OCTAVES.toFloat()).toInt()

fun potential3(p: NoiseVec3, octaves: Float, lacunarity: Float, gain: Float)

The vector potential curl3 differentiates: one scalar fbm read at three widely separated offsets. Using one field three times rather than three fields is deliberate — it is cheaper, and the offsets are far enough apart that the components are uncorrelated.

    fun potential3(p: NoiseVec3, octaves: Float, lacunarity: Float, gain: Float) = NoiseVec3(
        fbm3(p + K.FIELD_OFFSET_A, octaves, lacunarity, gain),
        fbm3(p + K.FIELD_OFFSET_B, octaves, lacunarity, gain),
        fbm3(p + K.FIELD_OFFSET_C, octaves, lacunarity, gain)
    )

fun curl3

Curl of potential3, central differences at one shared epsilon.

The shared epsilon is the whole point. Written this way the operator is (D_y P_z - D_z P_y, D_z P_x - D_x P_z, D_x P_y - D_y P_x) where each D_i is the same central-difference shift operator. Shift operators commute, so applying the same D_i again to take a divergence produces six terms that cancel in pairs and the result is identically zero — not small, zero, up to float rounding. Mixing epsilons, or taking the divergence with a different stencil, throws that away and leaves an O(e^2) residue.

    fun curl3(
        p: NoiseVec3,
        epsilon: Float = K.DEFAULT_CURL_EPSILON,
        octaves: Float = K.DEFAULT_CURL_OCTAVES,
        lacunarity: Float = K.DEFAULT_LACUNARITY,
        gain: Float = K.DEFAULT_GAIN
    ): NoiseVec3 {
        val e = maxOf(abs(epsilon), 1.0e-4f)
        val inv = 1.0f / (2.0f * e)
        val dx = (potential3(p + NoiseVec3(e, 0f, 0f), octaves, lacunarity, gain) -
            potential3(p - NoiseVec3(e, 0f, 0f), octaves, lacunarity, gain)) * inv
        val dy = (potential3(p + NoiseVec3(0f, e, 0f), octaves, lacunarity, gain) -
            potential3(p - NoiseVec3(0f, e, 0f), octaves, lacunarity, gain)) * inv
        val dz = (potential3(p + NoiseVec3(0f, 0f, e), octaves, lacunarity, gain) -
            potential3(p - NoiseVec3(0f, 0f, e), octaves, lacunarity, gain)) * inv
        return NoiseVec3(dy.z - dz.y, dz.x - dx.z, dx.y - dy.x)
    }

fun worley3(p: NoiseVec3): NoiseVec4

Cellular noise over a jittered lattice, packed so that the useful channels are all present at once: .x is F1, the distance to the nearest feature point; .y is F2, the second nearest; .z is F2 - F1, which is near zero exactly on the cell walls and is the crack or Voronoi-edge field; .w is a per-cell random constant, for flat-shading the cells.

A 3x3x3 search is exact whenever F1 is below 1.0, which it is for the overwhelming majority of points; beyond that it can miss a nearer point two cells away.

    fun worley3(p: NoiseVec3): NoiseVec4 {
        val ix = floor(p.x)
        val iy = floor(p.y)
        val iz = floor(p.z)
        val fx = p.x - ix
        val fy = p.y - iy
        val fz = p.z - iz
        var f1 = 1.0e9f
        var f2 = 1.0e9f
        var cell = 0.0f
        for (gz in -1..1) {
            for (gy in -1..1) {
                for (gx in -1..1) {
                    val cx = ix + gx
                    val cy = iy + gy
                    val cz = iz + gz
                    val o = hash33(cx, cy, cz)
                    val d = NoiseVec3(gx + o.x - fx, gy + o.y - fy, gz + o.z - fz).length
                    if (d < f1) {
                        f2 = f1
                        f1 = d
                        cell = hash31(cx, cy, cz)
                    } else if (d < f2) {
                        f2 = d
                    }
                }
            }
        }
        return NoiseVec4(f1, f2, f2 - f1, cell)
    }

fun fbmd3

fbm3 and its gradient in one word: .x is the value, .yzw the gradient. The gradient of a scalar field is a normal — of an isosurface, of a cloud edge — so this is the word to reach for when shading one, rather than differencing fbm3 by hand.

    fun fbmd3(
        p: NoiseVec3,
        epsilon: Float = K.DEFAULT_GRADIENT_EPSILON,
        octaves: Float = K.DEFAULT_OCTAVES,
        lacunarity: Float = K.DEFAULT_LACUNARITY,
        gain: Float = K.DEFAULT_GAIN
    ): NoiseVec4 {
        val e = maxOf(abs(epsilon), 1.0e-4f)
        val inv = 1.0f / (2.0f * e)
        val v = fbm3(p, octaves, lacunarity, gain)
        val gx = (fbm3(p + NoiseVec3(e, 0f, 0f), octaves, lacunarity, gain) -
            fbm3(p - NoiseVec3(e, 0f, 0f), octaves, lacunarity, gain)) * inv
        val gy = (fbm3(p + NoiseVec3(0f, e, 0f), octaves, lacunarity, gain) -
            fbm3(p - NoiseVec3(0f, e, 0f), octaves, lacunarity, gain)) * inv
        val gz = (fbm3(p + NoiseVec3(0f, 0f, e), octaves, lacunarity, gain) -
            fbm3(p - NoiseVec3(0f, 0f, e), octaves, lacunarity, gain)) * inv
        return NoiseVec4(v, gx, gy, gz)
    }

fun warp3

Domain warping: fbm3 evaluated at a position that has itself been displaced by an fbm3 vector field. One line of arithmetic that turns the characteristic cloudy fbm into something with filaments and folds. Still in [0,1), because the outer call is still fbm3.

    fun warp3(
        p: NoiseVec3,
        strength: Float = K.DEFAULT_WARP_STRENGTH,
        octaves: Float = K.DEFAULT_OCTAVES,
        lacunarity: Float = K.DEFAULT_LACUNARITY,
        gain: Float = K.DEFAULT_GAIN
    ): Float {
        val q = potential3(p, octaves, lacunarity, gain)
        val displaced = p + (q - NoiseVec3(0.5f, 0.5f, 0.5f)) * (2.0f * strength)
        return fbm3(displaced, octaves, lacunarity, gain)
    }
}

GLSL emission. Every numeric literal below is interpolated from VolumetricNoiseConstants; a bare float in this text is a bug, and the anti-drift test looks for exactly that.

internal val volumetricNoiseHelperDependencies: Map<String, List<String>>

Helper keys, and what each one needs emitted before it. Order of emission is dependency order.

internal val volumetricNoiseHelperDependencies: Map<String, List<String>> = linkedMapOf(
    "hash31" to emptyList(),
    "hash33" to emptyList(),
    "hash41" to emptyList(),
    "noise3" to listOf("hash31"),
    "noise4" to listOf("hash41"),
    "fbm3" to listOf("noise3"),
    "fbm4" to listOf("noise4"),
    "potential3" to listOf("fbm3"),
    "curl3" to listOf("potential3"),
    "fbmd3" to listOf("fbm3"),
    "warp3" to listOf("potential3"),
    "worley3" to listOf("hash31", "hash33")
)

internal fun expandVolumetricHelpers(requested: Set<String>): Set<String>

Expand a requested helper set to include everything it transitively needs, in emission order.

internal fun expandVolumetricHelpers(requested: Set<String>): Set<String> {
    val out = linkedSetOf<String>()
    fun visit(name: String) {
        if (name in out) return
        val deps = volumetricNoiseHelperDependencies[name] ?: return
        deps.forEach(::visit)
        out += name
    }
    // Iterate the declaration order, not the request order, so emission is deterministic.
    volumetricNoiseHelperDependencies.keys.forEach { if (it in requested) visit(it) }
    return out
}

internal fun glslFloatLiteral(value: Float): String

A GLSL float literal. Float.toString already gives the shortest text that round-trips to the same float, so this is exact as well as readable -- %.7f would have written 31.32 as 31.3199997. Exponent forms are expanded because GLSL will not take 1.0E-4.

Shared with the tests: the anti-drift check has to look for exactly the text the emitter wrote, so both sides must format through here.

internal fun glslFloatLiteral(value: Float): String {
    if (!value.isFinite()) return "0.0"
    val text = value.toString()
    val plain = if (text.contains('e', ignoreCase = true)) {
        java.math.BigDecimal(text).toPlainString()
    } else {
        text
    }
    return if (plain.contains('.')) plain else "$plain.0"
}

private fun Float.g(): String = glslFloatLiteral(this)

private fun NoiseVec3.g(): String = "vec3(${x.g()}, ${y.g()}, ${z.g()})"

internal fun volumetricNoiseHelperSource(prefix: String, helpers: Set<String>): String

The shader half of this file. Emitted only for the helpers a program actually reaches, which matters: curl3 alone pulls in eighteen fbm3 calls' worth of code, and a program that only says noise3 should not pay for it.

body — 192 lines, from line 544
internal fun volumetricNoiseHelperSource(prefix: String, helpers: Set<String>): String {
    val needed = expandVolumetricHelpers(helpers)
    if (needed.isEmpty()) return ""
    val K = VolumetricNoiseConstants
    val scale3 = "vec3(${K.HASH3_SCALE_X.g()}, ${K.HASH3_SCALE_Y.g()}, ${K.HASH3_SCALE_Z.g()})"
    val scale4 = "vec4(${K.HASH3_SCALE_X.g()}, ${K.HASH3_SCALE_Y.g()}, " +
        "${K.HASH3_SCALE_Z.g()}, ${K.HASH4_SCALE_W.g()})"
    val seed = K.HASH_SEED.g()
    val rot = "mat3(${K.OCTAVE_ROTATION_COLUMNS.joinToString(", ") { it.g() }})"
    val maxOct = K.MAX_OCTAVES

    return buildString {
        if ("hash31" in needed) {
            appendLine("float ${prefix}_hash31(vec3 p) {")
            appendLine("    p = fract((p + $seed) * $scale3);")
            appendLine("    p += dot(p, p.zyx + ${K.HASH31_ADD.g()});")
            appendLine("    return fract((p.x + p.y) * p.z);")
            appendLine("}")
            appendLine()
        }
        if ("hash33" in needed) {
            appendLine("vec3 ${prefix}_hash33(vec3 p) {")
            appendLine("    p = fract((p + $seed) * $scale3);")
            appendLine("    p += dot(p, p.yxz + ${K.HASH33_ADD.g()});")
            appendLine("    return fract((p.xxy + p.yxx) * p.zyx);")
            appendLine("}")
            appendLine()
        }
        if ("hash41" in needed) {
            appendLine("float ${prefix}_hash41(vec4 p) {")
            appendLine("    p = fract((p + $seed) * $scale4);")
            appendLine("    p += dot(p, p.wzxy + ${K.HASH41_ADD.g()});")
            appendLine("    return fract((p.x + p.y) * (p.z + p.w));")
            appendLine("}")
            appendLine()
        }
        if ("noise3" in needed) {
            appendLine("float ${prefix}_noise3(vec3 p) {")
            appendLine("    vec3 i = floor(p);")
            appendLine("    vec3 f = p - i;")
            appendLine("    vec3 u = f * f * (3.0 - 2.0 * f);")
            appendLine("    float c000 = ${prefix}_hash31(i + vec3(0.0, 0.0, 0.0));")
            appendLine("    float c100 = ${prefix}_hash31(i + vec3(1.0, 0.0, 0.0));")
            appendLine("    float c010 = ${prefix}_hash31(i + vec3(0.0, 1.0, 0.0));")
            appendLine("    float c110 = ${prefix}_hash31(i + vec3(1.0, 1.0, 0.0));")
            appendLine("    float c001 = ${prefix}_hash31(i + vec3(0.0, 0.0, 1.0));")
            appendLine("    float c101 = ${prefix}_hash31(i + vec3(1.0, 0.0, 1.0));")
            appendLine("    float c011 = ${prefix}_hash31(i + vec3(0.0, 1.0, 1.0));")
            appendLine("    float c111 = ${prefix}_hash31(i + vec3(1.0, 1.0, 1.0));")
            appendLine("    float z0 = mix(mix(c000, c100, u.x), mix(c010, c110, u.x), u.y);")
            appendLine("    float z1 = mix(mix(c001, c101, u.x), mix(c011, c111, u.x), u.y);")
            appendLine("    return mix(z0, z1, u.z);")
            appendLine("}")
            appendLine()
        }
        if ("noise4" in needed) {
            appendLine("float ${prefix}_noise4_slice(vec4 i, vec3 u) {")
            appendLine("    float c000 = ${prefix}_hash41(i + vec4(0.0, 0.0, 0.0, 0.0));")
            appendLine("    float c100 = ${prefix}_hash41(i + vec4(1.0, 0.0, 0.0, 0.0));")
            appendLine("    float c010 = ${prefix}_hash41(i + vec4(0.0, 1.0, 0.0, 0.0));")
            appendLine("    float c110 = ${prefix}_hash41(i + vec4(1.0, 1.0, 0.0, 0.0));")
            appendLine("    float c001 = ${prefix}_hash41(i + vec4(0.0, 0.0, 1.0, 0.0));")
            appendLine("    float c101 = ${prefix}_hash41(i + vec4(1.0, 0.0, 1.0, 0.0));")
            appendLine("    float c011 = ${prefix}_hash41(i + vec4(0.0, 1.0, 1.0, 0.0));")
            appendLine("    float c111 = ${prefix}_hash41(i + vec4(1.0, 1.0, 1.0, 0.0));")
            appendLine("    float z0 = mix(mix(c000, c100, u.x), mix(c010, c110, u.x), u.y);")
            appendLine("    float z1 = mix(mix(c001, c101, u.x), mix(c011, c111, u.x), u.y);")
            appendLine("    return mix(z0, z1, u.z);")
            appendLine("}")
            appendLine()
            appendLine("float ${prefix}_noise4(vec4 p) {")
            appendLine("    vec4 i = floor(p);")
            appendLine("    vec4 f = p - i;")
            appendLine("    vec4 u = f * f * (3.0 - 2.0 * f);")
            appendLine("    float s0 = ${prefix}_noise4_slice(i, u.xyz);")
            appendLine("    float s1 = ${prefix}_noise4_slice(i + vec4(0.0, 0.0, 0.0, 1.0), u.xyz);")
            appendLine("    return mix(s0, s1, u.w);")
            appendLine("}")
            appendLine()
        }
        if ("fbm3" in needed) {
            appendLine("float ${prefix}_fbm3(vec3 p, float octaves, float lacunarity, float gain) {")
            appendLine("    int n = int(clamp(octaves, 1.0, ${maxOct.toFloat().g()}));")
            appendLine("    float amp = 1.0;")
            appendLine("    float sum = 0.0;")
            appendLine("    float norm = 0.0;")
            appendLine("    for (int i = 0; i < $maxOct; i++) {")
            appendLine("        if (i >= n) break;")
            appendLine("        sum += amp * ${prefix}_noise3(p);")
            appendLine("        norm += amp;")
            appendLine("        p = $rot * p * lacunarity + ${K.OCTAVE_OFFSET.g()};")
            appendLine("        amp *= gain;")
            appendLine("    }")
            appendLine("    return norm > 0.0 ? sum / norm : 0.0;")
            appendLine("}")
            appendLine()
        }
        if ("fbm4" in needed) {
            appendLine("float ${prefix}_fbm4(vec4 p, float octaves, float lacunarity, float gain) {")
            appendLine("    int n = int(clamp(octaves, 1.0, ${maxOct.toFloat().g()}));")
            appendLine("    float amp = 1.0;")
            appendLine("    float sum = 0.0;")
            appendLine("    float norm = 0.0;")
            appendLine("    for (int i = 0; i < $maxOct; i++) {")
            appendLine("        if (i >= n) break;")
            appendLine("        sum += amp * ${prefix}_noise4(p);")
            appendLine("        norm += amp;")
            appendLine("        p = vec4($rot * p.xyz * lacunarity + ${K.OCTAVE_OFFSET.g()},")
            appendLine("                 p.w * lacunarity + ${K.OCTAVE_OFFSET_W.g()});")
            appendLine("        amp *= gain;")
            appendLine("    }")
            appendLine("    return norm > 0.0 ? sum / norm : 0.0;")
            appendLine("}")
            appendLine()
        }
        if ("potential3" in needed) {
            appendLine("vec3 ${prefix}_potential3(vec3 p, float octaves, float lacunarity, float gain) {")
            appendLine("    return vec3(")
            appendLine("        ${prefix}_fbm3(p + ${K.FIELD_OFFSET_A.g()}, octaves, lacunarity, gain),")
            appendLine("        ${prefix}_fbm3(p + ${K.FIELD_OFFSET_B.g()}, octaves, lacunarity, gain),")
            appendLine("        ${prefix}_fbm3(p + ${K.FIELD_OFFSET_C.g()}, octaves, lacunarity, gain));")
            appendLine("}")
            appendLine()
        }
        if ("curl3" in needed) {
            appendLine("// Central differences at ONE shared epsilon: the discrete shift operators then")
            appendLine("// commute, so the discrete divergence of this cancels term for term and is zero.")
            appendLine("vec3 ${prefix}_curl3(vec3 p, float epsilon, float octaves, float lacunarity, float gain) {")
            appendLine("    float e = max(abs(epsilon), 0.0001);")
            appendLine("    float inv = 1.0 / (2.0 * e);")
            appendLine("    vec3 dx = (${prefix}_potential3(p + vec3(e, 0.0, 0.0), octaves, lacunarity, gain)")
            appendLine("             - ${prefix}_potential3(p - vec3(e, 0.0, 0.0), octaves, lacunarity, gain)) * inv;")
            appendLine("    vec3 dy = (${prefix}_potential3(p + vec3(0.0, e, 0.0), octaves, lacunarity, gain)")
            appendLine("             - ${prefix}_potential3(p - vec3(0.0, e, 0.0), octaves, lacunarity, gain)) * inv;")
            appendLine("    vec3 dz = (${prefix}_potential3(p + vec3(0.0, 0.0, e), octaves, lacunarity, gain)")
            appendLine("             - ${prefix}_potential3(p - vec3(0.0, 0.0, e), octaves, lacunarity, gain)) * inv;")
            appendLine("    return vec3(dy.z - dz.y, dz.x - dx.z, dx.y - dy.x);")
            appendLine("}")
            appendLine()
        }
        if ("fbmd3" in needed) {
            appendLine("vec4 ${prefix}_fbmd3(vec3 p, float epsilon, float octaves, float lacunarity, float gain) {")
            appendLine("    float e = max(abs(epsilon), 0.0001);")
            appendLine("    float inv = 1.0 / (2.0 * e);")
            appendLine("    float v = ${prefix}_fbm3(p, octaves, lacunarity, gain);")
            appendLine("    float gx = (${prefix}_fbm3(p + vec3(e, 0.0, 0.0), octaves, lacunarity, gain)")
            appendLine("              - ${prefix}_fbm3(p - vec3(e, 0.0, 0.0), octaves, lacunarity, gain)) * inv;")
            appendLine("    float gy = (${prefix}_fbm3(p + vec3(0.0, e, 0.0), octaves, lacunarity, gain)")
            appendLine("              - ${prefix}_fbm3(p - vec3(0.0, e, 0.0), octaves, lacunarity, gain)) * inv;")
            appendLine("    float gz = (${prefix}_fbm3(p + vec3(0.0, 0.0, e), octaves, lacunarity, gain)")
            appendLine("              - ${prefix}_fbm3(p - vec3(0.0, 0.0, e), octaves, lacunarity, gain)) * inv;")
            appendLine("    return vec4(v, gx, gy, gz);")
            appendLine("}")
            appendLine()
        }
        if ("warp3" in needed) {
            appendLine("float ${prefix}_warp3(vec3 p, float strength, float octaves, float lacunarity, float gain) {")
            appendLine("    vec3 q = ${prefix}_potential3(p, octaves, lacunarity, gain);")
            appendLine("    return ${prefix}_fbm3(p + (q - vec3(0.5)) * (2.0 * strength), octaves, lacunarity, gain);")
            appendLine("}")
            appendLine()
        }
        if ("worley3" in needed) {
            appendLine("// .x = F1, .y = F2, .z = F2 - F1 (the cell-wall crack field), .w = a per-cell constant.")
            appendLine("vec4 ${prefix}_worley3(vec3 p) {")
            appendLine("    vec3 i = floor(p);")
            appendLine("    vec3 f = p - i;")
            appendLine("    float f1 = 1000000000.0;")
            appendLine("    float f2 = 1000000000.0;")
            appendLine("    float cell = 0.0;")
            appendLine("    for (int gz = -1; gz <= 1; gz++) {")
            appendLine("        for (int gy = -1; gy <= 1; gy++) {")
            appendLine("            for (int gx = -1; gx <= 1; gx++) {")
            appendLine("                vec3 g = vec3(float(gx), float(gy), float(gz));")
            appendLine("                vec3 c = i + g;")
            appendLine("                float d = length(g + ${prefix}_hash33(c) - f);")
            appendLine("                if (d < f1) {")
            appendLine("                    f2 = f1;")
            appendLine("                    f1 = d;")
            appendLine("                    cell = ${prefix}_hash31(c);")
            appendLine("                } else if (d < f2) {")
            appendLine("                    f2 = d;")
            appendLine("                }")
            appendLine("            }")
            appendLine("        }")
            appendLine("    }")
            appendLine("    return vec4(f1, f2, f2 - f1, cell);")
            appendLine("}")
            appendLine()
        }
    }
}