GLSL Procedural Wood Grain with Cylindrical Distance
Procedural wood grain generation in GLSL simulates the natural biological growth of tree rings by calculating the radial distance from a central axis in three-dimensional object space. By utilizing a cylindrical distance metric, perturbing that metric with continuous noise functions, and mapping the resulting values to wood-tone color gradients, shaders can render realistic, non-repeating concentric grain patterns directly onto 3D surfaces without requiring external texture maps.
Establishing the Cylindrical Coordinate Base
Real tree trunks grow outward from a central vertical axis, forming concentric cylindrical layers. In 3D space, this core axis is typically mapped along the Y-axis. The distance \(r\) from any point \(P(x, y, z)\) to the Y-axis is determined using the 2D Euclidean distance on the XZ plane:
float radius = length(position.xz);This radial distance value forms the baseline coordinate for generating concentric rings. Because it ignores the Y-component, the basic shape forms an infinitely tall, perfect geometric cylinder.
Introducing Biological Imperfection with Noise
Natural wood exhibits grain distortions, knots, and longitudinal variations. A raw geometric distance creates perfectly smooth, artificial concentric bands. To introduce organic variation, procedural noise—such as Perlin noise, Simplex noise, or Fractal Brownian Motion (FBM)—is added to perturb the coordinates before or during the distance calculation.
There are two primary approaches to perturbation:
Domain Warping: Displacing the original position coordinates using 3D noise before evaluating the length:
vec3 warpedPos = position + noise3D(position * noiseScale) * distortionStrength; float radius = length(warpedPos.xz);Radius Distortion: Directly modifying the computed radius with noise dependent on the longitudinal axis (\(Y\)):
float radius = length(position.xz) + noise3D(vec3(position.xz * 2.0, position.y * 0.1)) * distortionStrength;
Scaling the noise inputs unevenly—stretching along the Y-axis while keeping the X and Z axes compact—reproduces the long, directional streaks characteristic of split wood lumber.
Ring Modulation and Pattern Repetition
Once the distorted radius is established, it must be transformed into
a recurring pattern of alternating rings (earlywood and latewood). This
is typically achieved using periodic mathematical functions such as
sin() or the fractional component function
fract().
Using a sine function yields smooth transitions between rings:
float ringPattern = sin(radius * ringFrequency);
ringPattern = ringPattern * 0.5 + 0.5; // Remap from [-1, 1] to [0, 1]To create sharper transitions where the harder latewood abruptly stops before the softer earlywood begins, high-frequency exponentiation or an asymmetric shaping curve can be applied:
float ringPattern = fract(radius * ringFrequency);
ringPattern = pow(ringPattern, 0.8); // Adjust edge sharpnessFine Grain Details and Color Mapping
Wood micro-structure consists of fine fibers running parallel to the trunk. A high-frequency, anisotropic noise stretched along the Y-axis can be added to modulate the surface brightness, simulating these capillary fibers.
Finally, the synthesized scalar value maps into a color palette using
the mix() function. Typically, shaders interpolate between
at least two colors representing the lighter earlywood and darker
latewood bands:
vec3 earlyWoodColor = vec3(0.85, 0.65, 0.45);
vec3 lateWoodColor = vec3(0.50, 0.30, 0.15);
// Composite scalar computation
float finalGrain = clamp(ringPattern + fiberNoise * 0.1, 0.0, 1.0);
vec3 woodColor = mix(earlyWoodColor, lateWoodColor, finalGrain);Complete Implementation Example
The following GLSL function demonstrates the end-to-end integration of cylindrical distance, domain warping, and color interpolation:
// Assumes a defined gradientNoise3D(vec3 p) function returning [-1.0, 1.0]
vec3 generateWood(vec3 worldPos) {
// 1. Scale coordinates for trunk size and longitudinal stretch
vec3 p = worldPos * vec3(2.0, 0.2, 2.0);
// 2. Domain warp using noise
vec3 distortion = vec3(
gradientNoise3D(p),
gradientNoise3D(p + vec3(43.12)),
gradientNoise3D(p + vec3(11.54))
);
p += distortion * 0.35;
// 3. Cylindrical distance metric (distance from Y axis)
float r = length(p.xz);
// 4. Ring generation with asymmetric falloff
float rings = fract(r * 8.0);
rings = smoothstep(0.0, 0.7, rings) - smoothstep(0.7, 1.0, rings);
// 5. High-frequency longitudinal fibers
float fibers = gradientNoise3D(worldPos * vec3(40.0, 2.0, 40.0)) * 0.08;
// 6. Color blending
float blendFactor = clamp(rings + fibers, 0.0, 1.0);
vec3 lightWood = vec3(0.76, 0.58, 0.38);
vec3 darkWood = vec3(0.42, 0.24, 0.12);
return mix(lightWood, darkWood, blendFactor);
}By controlling the origin of the XZ plane, the angle of the cutting surface, and the noise scales, this implementation can accurately represent different cuts of wood, such as quarter-sawn, flat-sawn, or end-grain lumber.