// ============================================================================
// BLENDER'S PERLIN NOISE -- ONE TEXT, COMPILED AS BOTH GLSL AND C++.
//
// From the "Perlin Noise" section of
// `source/blender/blenlib/intern/noise.cc`, Blender source commit 61a36e08,
// BLENDER_VERSION 502. Blender's own header notes it is functionally identical
// to the EEVEE, OSL and SVM implementations, so any change belongs in all of
// them; here it needs to stay identical to Blender's, which is the point.
//
// DEPENDS ON blender_hash.inc and does not include it: GLSL has no #include,
// so the emitter concatenates hash-then-perlin and the C++ shim includes them
// in that order. Perlin's lattice gradients ARE that hash.
//
// Two things in here could not have been guessed:
//
// the per-dimension SCALE CONSTANTS on perlin_signed -- 0.2500, 0.6616,
// 0.9820, 0.8344 -- which the OSL developers found experimentally to remap
// each dimension's output to [-1, 1]. 0.9820 is the 3D one, and it is the
// same 0.982 that appeared as an unexplained "normalization factor matching
// iq/Blender" in ProcNodes_Noised, where it did not belong.
//
// the SEEDS of the Colour output's second and third components, which differ
// per dimension: 1D uses offsets 1 and 2, 2D uses 2 and 3, 3D uses 3 and 4,
// 4D uses 4 and 5. Blender calls them "arbitrary seeds ... for compatibility
// with shading functions", and there is no rule to derive them from.
//
// ---------------------------------------------------------------------------
// THE SUBSET, AND THE THREE WAYS OUT OF IT
//
// 1. C++ TEMPLATES. Five of these functions are `template <typename T>` and
// GLSL has none, so each becomes four per-dimension copies. Writing those
// twenty bodies by hand is exactly the transcription this file exists to
// avoid -- one wrong suffix and a dimension silently evaluates another
// one -- so the body is written ONCE and the preprocessor expands it,
// with `##` for the suffix. Verified to compile under glslc/glslang,
// which is what both the gate and Godot's RenderingDevice use. The
// functions that are OVERLOADS in Blender's C++ stay overloads here; only
// the templates are macros, so the structure still matches the source.
//
// 2. THE REFERENCE OUT-PARAMETER. floor_fraction returned the fraction and
// wrote the integer through `int &`. GLSL's `inout` is not the same
// thing, so it is two functions over the same floor().
//
// 3. fmodf IS TRUNCATED AND GLSL's mod() IS FLOORED. They differ for
// negative operands, which perlin_signed feeds constantly. bp_fmod
// spells out the truncated form; writing mod() would wrap the texture
// differently on the two sides of the origin.
//
// ⚠ EVERY DECIMAL LITERAL CARRIES AN `f`. Without it, `0.9820` is a float in
// GLSL and a DOUBLE in C++, so the CPU would compute an octave in double and
// the GPU in float. GLSL accepts the suffix (measured, glslc 2023.8) and it
// makes both languages parse the same float. The four constants that are not
// exactly representable -- 0.6616, 0.9820, 0.8344, 0.001 -- happen to round
// the same either way, measured, but relying on that is a coin the next
// constant need not land on.
// ============================================================================
#ifndef PN_BLENDER_PERLIN
#define PN_BLENDER_PERLIN
#ifndef BN_FN
#define BN_FN
#endif
const int BP_MULTIFRACTAL = 0;
const int BP_FBM = 1;
const int BP_HYBRID_MULTIFRACTAL = 2;
const int BP_RIDGED_MULTIFRACTAL = 3;
const int BP_HETERO_TERRAIN = 4;
// Blender's mix: (1 - x) * v0 + x * v1. NOT GLSL's mix(), which is
// v0 + x * (v1 - v0) -- algebraically the same and not the same in floating
// point, and this is the interpolation every octave passes through.
BN_FN float bp_pmix(float v0, float v1, float x) { return (1.0f - x) * v0 + x * v1; }
BN_FN float bp_pmix(float v0, float v1, float v2, float v3, float x, float y) {
float x1 = 1.0f - x;
return (1.0f - y) * (v0 * x1 + v1 * x) + y * (v2 * x1 + v3 * x);
}
BN_FN float bp_pmix(float v0, float v1, float v2, float v3, float v4, float v5, float v6,
float v7, float x, float y, float z) {
float x1 = 1.0f - x;
float y1 = 1.0f - y;
float z1 = 1.0f - z;
return z1 * (y1 * (v0 * x1 + v1 * x) + y * (v2 * x1 + v3 * x)) +
z * (y1 * (v4 * x1 + v5 * x) + y * (v6 * x1 + v7 * x));
}
BN_FN float bp_pmix(float v0, float v1, float v2, float v3, float v4, float v5, float v6,
float v7, float v8, float v9, float v10, float v11, float v12, float v13, float v14,
float v15, float x, float y, float z, float w) {
return bp_pmix(bp_pmix(v0, v1, v2, v3, v4, v5, v6, v7, x, y, z),
bp_pmix(v8, v9, v10, v11, v12, v13, v14, v15, x, y, z), w);
}
BN_FN float bp_fade(float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }
BN_FN float bp_negate_if(float value, uint condition) {
return (condition != 0u) ? -value : value;
}
BN_FN float bp_noise_grad(uint hash_v, float x) {
uint h = hash_v & 15u;
float g = 1.0f + float(h & 7u);
return bp_negate_if(g, h & 8u) * x;
}
BN_FN float bp_noise_grad(uint hash_v, float x, float y) {
uint h = hash_v & 7u;
float u = h < 4u ? x : y;
float v = 2.0f * (h < 4u ? y : x);
return bp_negate_if(u, h & 1u) + bp_negate_if(v, h & 2u);
}
BN_FN float bp_noise_grad(uint hash_v, float x, float y, float z) {
uint h = hash_v & 15u;
float u = h < 8u ? x : y;
float vt = (h == 12u || h == 14u) ? x : z;
float v = h < 4u ? y : vt;
return bp_negate_if(u, h & 1u) + bp_negate_if(v, h & 2u);
}
BN_FN float bp_noise_grad(uint hash_v, float x, float y, float z, float w) {
uint h = hash_v & 31u;
float u = h < 24u ? x : y;
float v = h < 16u ? y : z;
float s = h < 8u ? z : w;
return bp_negate_if(u, h & 1u) + bp_negate_if(v, h & 2u) + bp_negate_if(s, h & 4u);
}
// floor_fraction, split because it wrote its integer through a reference.
BN_FN int bp_floor_int(float x) { return int(floor(x)); }
BN_FN float bp_floor_fract(float x) { return x - floor(x); }
// TRUNCATED, like fmodf. GLSL's mod() is floored and would wrap the texture
// differently either side of the origin.
BN_FN float bp_fmod(float a, float b) { return a - b * trunc(a / b); }
BN_FN float bp_perlin_noise(float position) {
int X = bp_floor_int(position);
float fx = bp_floor_fract(position);
float u = bp_fade(fx);
return bp_pmix(bp_noise_grad(bn_hash(uint(X)), fx),
bp_noise_grad(bn_hash(uint(X + 1)), fx - 1.0f), u);
}
BN_FN float bp_perlin_noise(vec2 position) {
int X = bp_floor_int(position.x);
int Y = bp_floor_int(position.y);
float fx = bp_floor_fract(position.x);
float fy = bp_floor_fract(position.y);
float u = bp_fade(fx);
float v = bp_fade(fy);
return bp_pmix(bp_noise_grad(bn_hash(uint(X), uint(Y)), fx, fy),
bp_noise_grad(bn_hash(uint(X + 1), uint(Y)), fx - 1.0f, fy),
bp_noise_grad(bn_hash(uint(X), uint(Y + 1)), fx, fy - 1.0f),
bp_noise_grad(bn_hash(uint(X + 1), uint(Y + 1)), fx - 1.0f, fy - 1.0f), u, v);
}
BN_FN float bp_perlin_noise(vec3 position) {
int X = bp_floor_int(position.x);
int Y = bp_floor_int(position.y);
int Z = bp_floor_int(position.z);
float fx = bp_floor_fract(position.x);
float fy = bp_floor_fract(position.y);
float fz = bp_floor_fract(position.z);
float u = bp_fade(fx);
float v = bp_fade(fy);
float w = bp_fade(fz);
uint x0 = uint(X); uint x1 = uint(X + 1);
uint y0 = uint(Y); uint y1 = uint(Y + 1);
uint z0 = uint(Z); uint z1 = uint(Z + 1);
return bp_pmix(bp_noise_grad(bn_hash(x0, y0, z0), fx, fy, fz),
bp_noise_grad(bn_hash(x1, y0, z0), fx - 1.0f, fy, fz),
bp_noise_grad(bn_hash(x0, y1, z0), fx, fy - 1.0f, fz),
bp_noise_grad(bn_hash(x1, y1, z0), fx - 1.0f, fy - 1.0f, fz),
bp_noise_grad(bn_hash(x0, y0, z1), fx, fy, fz - 1.0f),
bp_noise_grad(bn_hash(x1, y0, z1), fx - 1.0f, fy, fz - 1.0f),
bp_noise_grad(bn_hash(x0, y1, z1), fx, fy - 1.0f, fz - 1.0f),
bp_noise_grad(bn_hash(x1, y1, z1), fx - 1.0f, fy - 1.0f, fz - 1.0f), u, v, w);
}
BN_FN float bp_perlin_noise(vec4 position) {
int X = bp_floor_int(position.x);
int Y = bp_floor_int(position.y);
int Z = bp_floor_int(position.z);
int W = bp_floor_int(position.w);
float fx = bp_floor_fract(position.x);
float fy = bp_floor_fract(position.y);
float fz = bp_floor_fract(position.z);
float fw = bp_floor_fract(position.w);
float u = bp_fade(fx);
float v = bp_fade(fy);
float t = bp_fade(fz);
float s = bp_fade(fw);
uint x0 = uint(X); uint x1 = uint(X + 1);
uint y0 = uint(Y); uint y1 = uint(Y + 1);
uint z0 = uint(Z); uint z1 = uint(Z + 1);
uint w0 = uint(W); uint w1 = uint(W + 1);
return bp_pmix(bp_noise_grad(bn_hash(x0, y0, z0, w0), fx, fy, fz, fw),
bp_noise_grad(bn_hash(x1, y0, z0, w0), fx - 1.0f, fy, fz, fw),
bp_noise_grad(bn_hash(x0, y1, z0, w0), fx, fy - 1.0f, fz, fw),
bp_noise_grad(bn_hash(x1, y1, z0, w0), fx - 1.0f, fy - 1.0f, fz, fw),
bp_noise_grad(bn_hash(x0, y0, z1, w0), fx, fy, fz - 1.0f, fw),
bp_noise_grad(bn_hash(x1, y0, z1, w0), fx - 1.0f, fy, fz - 1.0f, fw),
bp_noise_grad(bn_hash(x0, y1, z1, w0), fx, fy - 1.0f, fz - 1.0f, fw),
bp_noise_grad(bn_hash(x1, y1, z1, w0), fx - 1.0f, fy - 1.0f, fz - 1.0f, fw),
bp_noise_grad(bn_hash(x0, y0, z0, w1), fx, fy, fz, fw - 1.0f),
bp_noise_grad(bn_hash(x1, y0, z0, w1), fx - 1.0f, fy, fz, fw - 1.0f),
bp_noise_grad(bn_hash(x0, y1, z0, w1), fx, fy - 1.0f, fz, fw - 1.0f),
bp_noise_grad(bn_hash(x1, y1, z0, w1), fx - 1.0f, fy - 1.0f, fz, fw - 1.0f),
bp_noise_grad(bn_hash(x0, y0, z1, w1), fx, fy, fz - 1.0f, fw - 1.0f),
bp_noise_grad(bn_hash(x1, y0, z1, w1), fx - 1.0f, fy, fz - 1.0f, fw - 1.0f),
bp_noise_grad(bn_hash(x0, y1, z1, w1), fx, fy - 1.0f, fz - 1.0f, fw - 1.0f),
bp_noise_grad(bn_hash(x1, y1, z1, w1), fx - 1.0f, fy - 1.0f, fz - 1.0f, fw - 1.0f),
u, v, t, s);
}
// The modulo repeats the texture every 100000 on each axis to keep the lattice
// coordinates representable, with a half-unit nudge past a million so the two
// sides of the wrap do not land on the same lattice point.
BN_FN float bp_perlin_signed(float position) {
float pc = 0.5f * float(abs(position) >= 1000000.0f);
float p = bp_fmod(position, 100000.0f) + pc;
return bp_perlin_noise(p) * 0.2500f;
}
BN_FN float bp_perlin_signed(vec2 position) {
vec2 pc = vec2(0.5f * float(abs(position.x) >= 1000000.0f),
0.5f * float(abs(position.y) >= 1000000.0f));
vec2 p = vec2(bp_fmod(position.x, 100000.0f), bp_fmod(position.y, 100000.0f)) + pc;
return bp_perlin_noise(p) * 0.6616f;
}
BN_FN float bp_perlin_signed(vec3 position) {
vec3 pc = vec3(0.5f * float(abs(position.x) >= 1000000.0f),
0.5f * float(abs(position.y) >= 1000000.0f),
0.5f * float(abs(position.z) >= 1000000.0f));
vec3 p = vec3(bp_fmod(position.x, 100000.0f), bp_fmod(position.y, 100000.0f),
bp_fmod(position.z, 100000.0f)) + pc;
return bp_perlin_noise(p) * 0.9820f;
}
BN_FN float bp_perlin_signed(vec4 position) {
vec4 pc = vec4(0.5f * float(abs(position.x) >= 1000000.0f),
0.5f * float(abs(position.y) >= 1000000.0f),
0.5f * float(abs(position.z) >= 1000000.0f),
0.5f * float(abs(position.w) >= 1000000.0f));
vec4 p = vec4(bp_fmod(position.x, 100000.0f), bp_fmod(position.y, 100000.0f),
bp_fmod(position.z, 100000.0f), bp_fmod(position.w, 100000.0f)) + pc;
return bp_perlin_noise(p) * 0.8344f;
}
// A float seed rather than an integer one, because OSL only has float hashes
// and this has to stay compatible with it.
BN_FN float bp_random_float_offset(float seed) {
return 100.0f + bn_hash_float_to_float(seed) * 100.0f;
}
BN_FN vec2 bp_random_float2_offset(float seed) {
return vec2(100.0f + bn_hash_float_to_float(vec2(seed, 0.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 1.0f)) * 100.0f);
}
BN_FN vec3 bp_random_float3_offset(float seed) {
return vec3(100.0f + bn_hash_float_to_float(vec2(seed, 0.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 1.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 2.0f)) * 100.0f);
}
BN_FN vec4 bp_random_float4_offset(float seed) {
return vec4(100.0f + bn_hash_float_to_float(vec2(seed, 0.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 1.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 2.0f)) * 100.0f,
100.0f + bn_hash_float_to_float(vec2(seed, 3.0f)) * 100.0f);
}
BN_FN float bp_perlin_distortion(float position, float strength) {
return bp_perlin_signed(position + bp_random_float_offset(0.0f)) * strength;
}
BN_FN vec2 bp_perlin_distortion(vec2 position, float strength) {
return vec2(bp_perlin_signed(position + bp_random_float2_offset(0.0f)) * strength,
bp_perlin_signed(position + bp_random_float2_offset(1.0f)) * strength);
}
BN_FN vec3 bp_perlin_distortion(vec3 position, float strength) {
return vec3(bp_perlin_signed(position + bp_random_float3_offset(0.0f)) * strength,
bp_perlin_signed(position + bp_random_float3_offset(1.0f)) * strength,
bp_perlin_signed(position + bp_random_float3_offset(2.0f)) * strength);
}
BN_FN vec4 bp_perlin_distortion(vec4 position, float strength) {
return vec4(bp_perlin_signed(position + bp_random_float4_offset(0.0f)) * strength,
bp_perlin_signed(position + bp_random_float4_offset(1.0f)) * strength,
bp_perlin_signed(position + bp_random_float4_offset(2.0f)) * strength,
bp_perlin_signed(position + bp_random_float4_offset(3.0f)) * strength);
}
// ---------------------------------------------------------------------------
// THE FIVE FRACTAL TYPES.
//
// `template <typename T>` in Blender's C++, and GLSL has no templates, so the
// body is written once here and the preprocessor stamps it out per dimension.
// T is the coordinate type, S the suffix -- bp_perlin_fbm_3 and so on.
// ---------------------------------------------------------------------------
#define BP_FRACTALS(T, S) \
BN_FN float bp_perlin_fbm_##S(T p, float detail, float roughness, float lacunarity, \
bool normalize) { \
float fscale = 1.0f; \
float amp = 1.0f; \
float maxamp = 0.0f; \
float sum = 0.0f; \
for (int i = 0; i <= int(detail); i++) { \
float t = bp_perlin_signed(p * fscale); \
sum += t * amp; \
maxamp += amp; \
amp *= roughness; \
fscale *= lacunarity; \
} \
float rmd = detail - floor(detail); \
if (rmd != 0.0f) { \
float t2 = bp_perlin_signed(p * fscale); \
float sum2 = sum + t2 * amp; \
if (normalize) { \
return bp_pmix(0.5f * sum / maxamp + 0.5f, \
0.5f * sum2 / (maxamp + amp) + 0.5f, rmd); \
} \
return bp_pmix(sum, sum2, rmd); \
} \
if (normalize) { \
return 0.5f * sum / maxamp + 0.5f; \
} \
return sum; \
} \
\
BN_FN float bp_perlin_multi_fractal_##S(T p, float detail, float roughness, float lacunarity) { \
float value = 1.0f; \
float pwr = 1.0f; \
for (int i = 0; i <= int(detail); i++) { \
value *= (pwr * bp_perlin_signed(p) + 1.0f); \
pwr *= roughness; \
p = p * lacunarity; \
} \
float rmd = detail - floor(detail); \
if (rmd != 0.0f) { \
value *= (rmd * pwr * bp_perlin_signed(p) + 1.0f); \
} \
return value; \
} \
\
BN_FN float bp_perlin_hetero_terrain_##S(T p, float detail, float roughness, float lacunarity, \
float offset) { \
float pwr = roughness; \
/* First unscaled octave of function; later octaves are scaled. */ \
float value = offset + bp_perlin_signed(p); \
p = p * lacunarity; \
for (int i = 1; i <= int(detail); i++) { \
float increment = (bp_perlin_signed(p) + offset) * pwr * value; \
value += increment; \
pwr *= roughness; \
p = p * lacunarity; \
} \
float rmd = detail - floor(detail); \
if (rmd != 0.0f) { \
float increment2 = (bp_perlin_signed(p) + offset) * pwr * value; \
value += rmd * increment2; \
} \
return value; \
} \
\
BN_FN float bp_perlin_hybrid_multi_fractal_##S(T p, float detail, float roughness, \
float lacunarity, float offset, float gain) { \
float pwr = 1.0f; \
float value = 0.0f; \
float weight = 1.0f; \
for (int i = 0; (weight > 0.001f) && (i <= int(detail)); i++) { \
weight = min(weight, 1.0f); \
float signal = (bp_perlin_signed(p) + offset) * pwr; \
pwr *= roughness; \
value += weight * signal; \
weight *= gain * signal; \
p = p * lacunarity; \
} \
float rmd = detail - floor(detail); \
if ((rmd != 0.0f) && (weight > 0.001f)) { \
weight = min(weight, 1.0f); \
float signal2 = (bp_perlin_signed(p) + offset) * pwr; \
value += rmd * weight * signal2; \
} \
return value; \
} \
\
BN_FN float bp_perlin_ridged_multi_fractal_##S(T p, float detail, float roughness, \
float lacunarity, float offset, float gain) { \
float pwr = roughness; \
float signal = offset - abs(bp_perlin_signed(p)); \
signal *= signal; \
float value = signal; \
float weight = 1.0f; \
for (int i = 1; i <= int(detail); i++) { \
p = p * lacunarity; \
weight = clamp(signal * gain, 0.0f, 1.0f); \
signal = offset - abs(bp_perlin_signed(p)); \
signal *= signal; \
signal *= weight; \
value += signal * pwr; \
pwr *= roughness; \
} \
return value; \
} \
\
BN_FN float bp_perlin_select_##S(T p, float detail, float roughness, float lacunarity, \
float offset, float gain, int type, bool normalize) { \
if (type == BP_MULTIFRACTAL) { \
return bp_perlin_multi_fractal_##S(p, detail, roughness, lacunarity); \
} \
if (type == BP_FBM) { \
return bp_perlin_fbm_##S(p, detail, roughness, lacunarity, normalize); \
} \
if (type == BP_HYBRID_MULTIFRACTAL) { \
return bp_perlin_hybrid_multi_fractal_##S(p, detail, roughness, lacunarity, offset, gain); \
} \
if (type == BP_RIDGED_MULTIFRACTAL) { \
return bp_perlin_ridged_multi_fractal_##S(p, detail, roughness, lacunarity, offset, gain); \
} \
if (type == BP_HETERO_TERRAIN) { \
return bp_perlin_hetero_terrain_##S(p, detail, roughness, lacunarity, offset); \
} \
return 0.0f; \
} \
\
BN_FN float bp_perlin_fractal_distorted_##S(T position, float detail, float roughness, \
float lacunarity, float offset, float gain, float distortion, int type, bool normalize) { \
position = position + bp_perlin_distortion(position, distortion); \
return bp_perlin_select_##S(position, detail, roughness, lacunarity, offset, gain, type, \
normalize); \
}
BP_FRACTALS(float, 1)
BP_FRACTALS(vec2, 2)
BP_FRACTALS(vec3, 3)
BP_FRACTALS(vec4, 4)
// THE COLOUR SEEDS DIFFER PER DIMENSION: 1D uses 1 and 2, 2D uses 2 and 3, 3D
// uses 3 and 4, 4D uses 4 and 5. Blender calls them "arbitrary seeds ... for
// compatibility with shading functions" and there is no rule behind them --
// which is why these four are written out rather than stamped from the macro.
BN_FN vec3 bp_perlin_float3_fractal_distorted(float position, float detail, float roughness,
float lacunarity, float offset, float gain, float distortion, int type, bool normalize) {
position += bp_perlin_distortion(position, distortion);
return vec3(
bp_perlin_select_1(position, detail, roughness, lacunarity, offset, gain, type, normalize),
bp_perlin_select_1(position + bp_random_float_offset(1.0f), detail, roughness,
lacunarity, offset, gain, type, normalize),
bp_perlin_select_1(position + bp_random_float_offset(2.0f), detail, roughness,
lacunarity, offset, gain, type, normalize));
}
BN_FN vec3 bp_perlin_float3_fractal_distorted(vec2 position, float detail, float roughness,
float lacunarity, float offset, float gain, float distortion, int type, bool normalize) {
position = position + bp_perlin_distortion(position, distortion);
return vec3(
bp_perlin_select_2(position, detail, roughness, lacunarity, offset, gain, type, normalize),
bp_perlin_select_2(position + bp_random_float2_offset(2.0f), detail, roughness,
lacunarity, offset, gain, type, normalize),
bp_perlin_select_2(position + bp_random_float2_offset(3.0f), detail, roughness,
lacunarity, offset, gain, type, normalize));
}
BN_FN vec3 bp_perlin_float3_fractal_distorted(vec3 position, float detail, float roughness,
float lacunarity, float offset, float gain, float distortion, int type, bool normalize) {
position = position + bp_perlin_distortion(position, distortion);
return vec3(
bp_perlin_select_3(position, detail, roughness, lacunarity, offset, gain, type, normalize),
bp_perlin_select_3(position + bp_random_float3_offset(3.0f), detail, roughness,
lacunarity, offset, gain, type, normalize),
bp_perlin_select_3(position + bp_random_float3_offset(4.0f), detail, roughness,
lacunarity, offset, gain, type, normalize));
}
BN_FN vec3 bp_perlin_float3_fractal_distorted(vec4 position, float detail, float roughness,
float lacunarity, float offset, float gain, float distortion, int type, bool normalize) {
position = position + bp_perlin_distortion(position, distortion);
return vec3(
bp_perlin_select_4(position, detail, roughness, lacunarity, offset, gain, type, normalize),
bp_perlin_select_4(position + bp_random_float4_offset(4.0f), detail, roughness,
lacunarity, offset, gain, type, normalize),
bp_perlin_select_4(position + bp_random_float4_offset(5.0f), detail, roughness,
lacunarity, offset, gain, type, normalize));
}
#endif // PN_BLENDER_PERLIN