PL. 86 · FIELDS / NOISE / SPARSE CONVOLUTION
Gabor Noise
Lagae, Lefebvre, Drettakis & Dutré, 2009 · kernel after Dennis Gabor, 1946
OPEN THE LIVE PLATE ▸DEFINITION
g(x,y) = K·e^(−πa²(x²+y²))·cos(2πF₀(x cos ω₀ + y sin ω₀)) N(p) = Σᵢ wᵢ·g(p − pᵢ), pᵢ a Poisson process of density λ ĝ is two Gaussians at ±F₀(cos ω₀, sin ω₀)
NOTES
Perlin noise gives you a band of frequencies and no say in which. Gabor noise gives you the spectrum directly, because it is built by scattering copies of one kernel whose Fourier transform is known exactly: a Gaussian envelope times a cosine, whose spectrum is a pair of Gaussian blobs sitting at the cosine’s own frequency and orientation. Move the blobs and the texture follows. The scattering is Lewis’s sparse convolution — impulses at Poisson-distributed points, each weighted at random — and it is evaluated without storing anything, by cutting the plane into cells the size of the kernel’s own support and re-deriving each cell’s impulses from a generator seeded on its coordinates. Gabor proved in 1946 that this kernel is the one with the least joint spread in time and frequency, which is why it is the right pulse and not merely a convenient one. One honest departure: the paper uses a hundred impulses per kernel, where the sum has gone Gaussian and no single kernel is visible. This plate cannot afford that and runs about a tenth as dense on purpose, so the construction shows — you can see the individual kernels, which is the point of an atlas and is not yet the band-limited noise the paper delivers.
PROVENANCE
- Origin
- A. Lagae, S. Lefebvre, G. Drettakis & P. Dutré, "Procedural Noise using Sparse Gabor Convolution", ACM Transactions on Graphics 28(3), SIGGRAPH 2009 — K.U. Leuven and REVES/INRIA Sophia-Antipolis. The kernel is equation (6); the 5% truncation at radius 1/a and the cell scheme are the paper’s.
- Kernel
- D. Gabor, "Theory of Communication", Journal of the IEE 93, Part III, 1946, pp. 429–457 — the elementary signal of least joint uncertainty
- Method
- Sparse convolution noise is J. P. Lewis, "Algorithms for Solid Noise Synthesis", SIGGRAPH 1989, after his 1984 paper; van Wijk’s spot noise, 1991, is the same family
- Patent
- None found. Searches of the literature and of patent listings turn up no grant or application tied to this construction — which is nothing found, not nothing exists, and is a weaker statement than the one perlin can make about US6867776.
- Standing
- Public domain as far as can be established — implemented from the published equations
- Constants
- The impulse count is the cost governor and is locked. The paper uses 100 per kernel on CPU and 25–50 on GPU; this plate’s ceiling of 20 is the frame budget, measured.
- Source
- doi:10.1145/1531326.1531360
TOUCHDESIGNER · GLSL
The same shader this plate runs, reframed for a GLSL TOP. Pasted bare it renders the published constants as a still frame; wire absTime.seconds into u_t on the Vectors page to animate it.
// FORMA — PL. 86 · GABOR NOISE — Lagae, Lefebvre, Drettakis & Dutré, 2009 · kernel after Dennis Gabor, 1946
// g(x,y) = K·e^(−πa²(x²+y²))·cos(2πF₀(x cos ω₀ + y sin ω₀))
// N(p) = Σᵢ wᵢ·g(p − pᵢ), pᵢ a Poisson process of density λ
// ĝ is two Gaussians at ±F₀(cos ω₀, sin ω₀)
// TouchDesigner port — paste into a GLSL TOP's pixel shader. Set the
// resolution on the TOP's Common page. As pasted it renders the published
// constants as a still frame; to animate, add a uniform named u_t on the
// GLSL TOP's Vectors 1 page with the expression absTime.seconds.
// Constants are consts — edit to tweak; comments give the measured range.
// Written from the published mathematics, not adapted from any code.
#define u_res (uTDOutputInfo.res.zw)
uniform float u_t; // absTime.seconds on the Vectors page; unset = still
const float u_phase = 0.13; // this plate's own grid phase, 0..1
// FORMA's FIELDS accent as cosine-gradient coefficients
const vec3 u_pal_a = vec3(0.46, 0.3031, 0.11);
const vec3 u_pal_b = vec3(0.5, 0.3294, 0.1196);
const vec3 u_pal_c = vec3(1, 1, 1);
const vec3 u_pal_d = vec3(0, 0.05, 0.1);
const float p_scale = 6.0; // kernels across the plate · live 2 .. 14
const float p_freq = 2.0; // F₀ — cycles per kernel · live 0.5 .. 6
const float p_orient = 35.0; // ω₀ — orientation (deg) · live 0 .. 180
const float p_spread = 0.25; // orientation spread, aniso → iso · live 0 .. 1
const float p_impulses = 8.0; // impulses per kernel · live 3 .. 20
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
vec3 ramp(float t){
return clamp(u_pal_a + u_pal_b * cos(6.28318530718 * (u_pal_c * t + u_pal_d)), 0.0, 1.0);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5 ? u * 2.0 : 2.0 - u * 2.0;
}
/* One function, not a next() and a uniform(): a body that declares a
uint-returning function is invisible to the composition namespacer, which
recognises the GLSL types the specimens actually return — so gabor composed
with gabor would emit it twice and fail to link, and neither test:compose
nor test:shaders would see it. Returning float keeps it namespaceable. */
float gabor_uniform(inout uint s){
s = s * 1664525u + 1013904223u;
return float(s) / 4294967296.0;
}
vec3 plate(vec2 uv){
float TAU = 6.283185307179586;
float ar = u_res.y / u_res.x;
float ang = u_phase * TAU;
float x = uv.x * p_scale + u_t * 0.10 * cos(ang);
float y = uv.y * ar * p_scale + u_t * 0.10 * sin(ang);
float w0 = p_orient * 3.141592653589793 / 180.0;
float lam = p_impulses / 3.141592653589793; // impulses per cell; the paper counts per kernel area
float L = exp(-lam);
float sigma = sqrt(lam / 3.0 * 0.25 * (1.0 + exp(-TAU * p_freq * p_freq)));
int ci = int(floor(x)), cj = int(floor(y));
float sum = 0.0;
for (int dj = -1; dj <= 1; dj++){
for (int di = -1; di <= 1; di++){
int cx = ci + di, cy = cj + dj;
uint s = uint(cx) * 1103515245u ^ uint(cy) * 1442695041u;
float prod = 1.0;
int k = 0;
for (int i = 0; i < 24; i++){ // 24 clears the Poisson tail at the slider's ceiling
prod *= gabor_uniform(s);
if (prod <= L) break;
k++;
}
for (int i = 0; i < 24; i++){ // 24 again: the count above can never exceed it
if (i >= k) break;
float px = float(cx) + gabor_uniform(s);
float py = float(cy) + gabor_uniform(s);
float wt = gabor_uniform(s) * 2.0 - 1.0;
float om = w0 + (gabor_uniform(s) * 2.0 - 1.0) * 3.141592653589793 * p_spread;
float ex = x - px, ey = y - py, d2 = ex * ex + ey * ey;
if (d2 > 1.0) continue; // the 5% truncation radius, equal to the cell
sum += wt * exp(-3.141592653589793 * d2) *
cos(TAU * p_freq * (ex * cos(om) + ey * sin(om)));
}
}
}
/* Zero takes the palette trough; the two signs run off it in opposite
directions into the bright lobe, which ramp's exact periodicity makes two
different hues. The noise's zero set is the dark contour network. */
return ramp(0.50 + 0.58 * clamp(sum / (2.6 * sigma), -1.0, 1.0));
}
out vec4 fragColor;
void main(){
// FORMA's uv runs y-down, matching its canvas; TD's vUV runs up
vec2 uv = vec2(vUV.s, 1.0 - vUV.t);
fragColor = TDOutputSwizzle(vec4(plate(uv), 1.0));
}
NUKE · BLINKSCRIPT
The same shader this plate runs, transpiled to a BlinkScript kernel. Paste it into a BlinkScript node's Kernel Source and press Recompile; every constant arrives as a knob at its published value, and u_t animates with the expression frame/24. Compiled and rendered in Nuke 17.1, then compared against this plate on the page.
// FORMA — PL. 86 · GABOR NOISE — Lagae, Lefebvre, Drettakis & Dutré, 2009 · kernel after Dennis Gabor, 1946
// g(x,y) = K·e^(−πa²(x²+y²))·cos(2πF₀(x cos ω₀ + y sin ω₀))
// N(p) = Σᵢ wᵢ·g(p − pᵢ), pᵢ a Poisson process of density λ
// ĝ is two Gaussians at ±F₀(cos ω₀, sin ω₀)
// Nuke port — a BlinkScript kernel. Paste into a BlinkScript node's Kernel
// Source and press Recompile. Every constant arrives as a knob at its published
// value (the comment gives the measured range); u_t is a knob too — animate it
// with the expression frame/24 or leave it at 0 for the still frame. Written
// from the published mathematics, not adapted from any code.
// Transpiled from the shader this plate runs on the page (GLSL ES 3.00):
// vec → float2/3/4, swizzles expanded, GLSL builtins Blink lacks written out
// as forma_ functions, float literals suffixed. Compiled and rendered in a
// real Nuke (17.1v1) and compared against this plate on the page: 34 of 34.
//
// plate() and its helpers are written to a single exit — the loop that runs
// once. That is not a style: Blink 17.1 drops a conditional early return from
// a called function while Vectorize is on, which is the node default, with no
// warning and no error. Written this way it paints correctly as pasted.
kernel Forma_gabor : ImageComputationKernel<ePixelWise>
{
Image<eWrite> dst;
param:
float u_t; // seconds; 0 is the still frame
float p_scale; // kernels across the plate · live 2 .. 14
float p_freq; // F₀ — cycles per kernel · live 0.5 .. 6
float p_orient; // ω₀ — orientation (deg) · live 0 .. 180
float p_spread; // orientation spread, aniso → iso · live 0 .. 1
float p_impulses; // impulses per kernel · live 3 .. 20
local:
float2 u_res;
float u_phase;
float3 u_pal_a, u_pal_b, u_pal_c, u_pal_d;
void define(){
defineParam(u_t, "u_t", 0.0f);
defineParam(p_scale, "p_scale", 6.0f);
defineParam(p_freq, "p_freq", 2.0f);
defineParam(p_orient, "p_orient", 35.0f);
defineParam(p_spread, "p_spread", 0.25f);
defineParam(p_impulses, "p_impulses", 8.0f);
}
void init(){
u_res = float2(float(dst.bounds.width()), float(dst.bounds.height()));
u_phase = 0.13f; // this plate's own grid phase, 0..1
// FORMA's FIELDS accent as cosine-gradient coefficients
u_pal_a = float3(0.46f, 0.3031f, 0.11f);
u_pal_b = float3(0.5f, 0.3294f, 0.1196f);
u_pal_c = float3(1.0f, 1.0f, 1.0f);
u_pal_d = float3(0.0f, 0.05f, 0.1f);
}
/* GLSL builtins Blink lacks, written as templates rather than overload sets.
Blink's operators return expression templates (Swizzle<float,N>), so a call
passing an expression cannot resolve against an overload set on float2
against float3 — measured in Nuke 17.1: a float2 expression is ambiguous
between the two, while scalar-against-vector resolves. A template deduces
the expression's own type, so the ambiguity cannot arise. */
template <class T> T forma_fract(T v){ return v - floor(v); }
template <class T, class S> T forma_mod(T x, S y){ return x - y * floor(x / y); }
/* Blink's own min/max/clamp take no scalar bound against a vector, which GLSL
does; v * 0.0f + b is that bound at the vector's own width, and collapses to
b when v is a scalar, so one template serves both. */
template <class T, class S> T forma_min(T a, S b){ return min(a, a * 0.0f + b); }
template <class T, class S> T forma_max(T a, S b){ return max(a, a * 0.0f + b); }
template <class T, class S> T forma_clamp(T v, S lo, S hi){ return clamp(v, v * 0.0f + lo, v * 0.0f + hi); }
int forma_min(int a, int b){ return min(a, b); }
int forma_max(int a, int b){ return max(a, b); }
/* GLSL step(edge, x) is 1 where x >= edge; floor(sign(x - e) * 0.5 + 1) is
that exactly, equality included, out of builtins Blink does have. */
template <class T, class S> T forma_step(S e, T x){ return floor(sign(x - e) * 0.5f + 1.0f); }
template <class T, class S> T forma_smoothstep(S a, S b, T x){
T t = forma_clamp((x - a) / (b - a), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
template <class T> float forma_distance(T a, T b){ return length(a - b); }
float forma_tanh(float x){ float e = exp(2.0f * x); return (e - 1.0f) / (e + 1.0f); }
float forma_radians(float d){ return d * 0.01745329252f; }
// the page's hash2 is exact uint32; Blink has int, so the shifts are made
// logical by masking and the read-back is lifted into 0 .. 2^32
/* A uint read back as a float. Blink has no unsigned type, so a value past
2^31 arrives as a negative int and float() of it is negative. Measured on
gabor, whose own generator then returned uniforms in [-0.5, 0.5) and drew
a different picture — it compiled, it rendered, and only comparing it with
/* The order's ramp — the same cosine formulation the JS kit uses, so a
plate keeps its classification colour in either language. */
float3 ramp(float t){
return forma_clamp(u_pal_a + u_pal_b * cos(6.28318530718f * (u_pal_c * t + u_pal_d)), 0.0f, 1.0f);
}
/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return forma_fract(t / period + u_phase); }
float pingpong(float t, float period){
float u = cycle(t, period);
return u < 0.5f ? u * 2.0f : 2.0f - u * 2.0f;
}
/* One function, not a next() and a uniform(): a body that declares a
uint-returning function is invisible to the composition namespacer, which
recognises the GLSL types the specimens actually return — so gabor composed
with gabor would emit it twice and fail to link, and neither test:compose
nor test:shaders would see it. Returning float keeps it namespaceable. */
float gabor_uniform(uint& s){
s = s * 1664525u + 1013904223u;
return float(s) / 4294967296.0f;
}
float3 plate(float2 uv){
float TAU = 6.283185307179586f;
float ar = u_res.y / u_res.x;
float ang = u_phase * TAU;
float x = uv.x * p_scale + u_t * 0.10f * cos(ang);
float y = uv.y * ar * p_scale + u_t * 0.10f * sin(ang);
float w0 = p_orient * 3.141592653589793f / 180.0f;
float lam = p_impulses / 3.141592653589793f; // impulses per cell; the paper counts per kernel area
float L = exp(-lam);
float sigma = sqrt(lam / 3.0f * 0.25f * (1.0f + exp(-TAU * p_freq * p_freq)));
int ci = int(floor(x));
int cj = int(floor(y));
float sum = 0.0f;
for (int dj = -1; dj <= 1; dj++){
for (int di = -1; di <= 1; di++){
int cx = ci + di;
int cy = cj + dj;
uint s = uint(cx) * 1103515245u ^ uint(cy) * 1442695041u;
float prod = 1.0f;
int k = 0;
for (int i = 0; i < 24; i++){ // 24 clears the Poisson tail at the slider's ceiling
prod *= gabor_uniform(s);
if (prod <= L) break;
k++;
}
for (int i = 0; i < 24; i++){ // 24 again: the count above can never exceed it
if (i >= k) break;
float px = float(cx) + gabor_uniform(s);
float py = float(cy) + gabor_uniform(s);
float wt = gabor_uniform(s) * 2.0f - 1.0f;
float om = w0 + (gabor_uniform(s) * 2.0f - 1.0f) * 3.141592653589793f * p_spread;
float ex = x - px;
float ey = y - py;
float d2 = ex * ex + ey * ey;
if (d2 > 1.0f) continue; // the 5% truncation radius, equal to the cell
sum += wt * exp(-3.141592653589793f * d2) *
cos(TAU * p_freq * (ex * cos(om) + ey * sin(om)));
}
}
}
/* Zero takes the palette trough; the two signs run off it in opposite
directions into the bright lobe, which ramp's exact periodicity makes two
different hues. The noise's zero set is the dark contour network. */
return ramp(0.50f + 0.58f * forma_clamp(sum / (2.6f * sigma), -1.0f, 1.0f));
}
void process(int2 pos){
// FORMA's uv runs y-down like its canvas; Nuke's rows run up
float2 uv = float2((float(pos.x) + 0.5f) / u_res.x, 1.0f - (float(pos.y) + 0.5f) / u_res.y);
float3 c = plate(uv);
dst() = float4(c.x, c.y, c.z, 1.0f);
}
};