Tiling Noise – Procedural GPU Patterns
Tiling Noise: Procedural GPU Patterns
This article represents the ninth installment of a tutorial series focused on generating procedural patterns via GPU shaders within the Godot Engine (version 4.7).
🍩 Value Noise on a Torus
Previously, our exploration of value noise was limited to flat planes. To expand this, we will now apply it to a torus.
Initial Setup
To begin, follow these steps to prepare the environment:
- Create a new scene named
value_noise_torus.tscn. - Model it similarly to
sine_waves_torus.tscn. - Apply a material consistent with the one found in
value_noise_plane.tscn. - Crucial: Keep the displacement at the default
0.2.- Note: Increasing this value will make the torus appear excessively thick.
The primary challenge here is that a torus possesses two distinct seams where the UV coordinates transition from to .
Upgrading Parameter Types
To allow for finer tuning on the torus (similar to our previous work with sine waves), we must upgrade the bumpiness and frequency uniforms from float to vec2 within value_noise.gdshader.
uniform vec2 bumpiness = vec2( 1.0 );
uniform float displacement : hint_range(-1.0, 1.0) = 0.2;
uniform bool vertex_displacement = true;
#define PATTERN_FREQUENCY_TYPE vec2
#define USE_STANDARD_FRACTAL_PATTERN_SETTINGS
We then modify the sample_value_noise() function to accommodate this new vec2 frequency type:
PatternSample sample_value_noise( int octave, vec2 frequency, vec2 uv, float time ) {
s.dx *= frequency.x;
s.dy *= frequency.y;
return s;
}
Configuration Settings
After updating the shader, the plane scene must be reconfigured. The following values are recommended for the torus:
| Parameter | X Value | Y Value |
|---|---|---|
| Bumpiness | ||
| Base Frequency |
🔍 Testing the UV Range
To achieve a seamless wrap, the pattern must tile, meaning it repeats identically for coordinates that fall below or exceed .
Initially, UV coordinates are set from to . To test the tiling effect, we will expand this range to go from to . To avoid duplicating code in the vertex() and fragment() functions, we introduce a helper function: sample_noise().
PatternSample sample_noise(vec2 uv) {
return sample_fractal_pattern( standard_fractal_pattern_settings(), uv * 3.0 - 1.0, TIME );
}
void vertex() {
if (vertex_displacement) {
PatternSample pattern_sample = sample_noise(UV);
VERTEX += NORMAL * (pattern_sample.v * displacement);
}
}
void fragment() {
PatternSample pattern_sample = sample_noise(UV);
ALBEDO = colorize(pattern_sample.v);
}
Since standard UVs range from to , this central tile is surrounded by eight others in a grid. Our objective is to ensure all nine tiles display the exact same pattern without any visible seams.
📐 Tiling Lattice Coordinates
While hash noise can be tiled simply by taking the fractional part of the UVs, value noise is more complex because it blends between adjacent lattice points. To eliminate seams, the lattice blocks at the boundaries must "wrap" their points to match the opposite side.
The Logic of Wrapping: If the frequency is , the standard lattice sequence is . This creates pairs: . To tile, the final point must wrap back to the start: . The new pairs become: .
Implementing lattice01()
We create a function to handle the first () and second () lattice coordinates for a single dimension:
vec2 lattice01(float lattice_coordinate, float frequency) {
float l0 = lattice_coordinate;
float l1 = l0 + 1.0;
return vec2(l0, l1);
}
We then integrate this into sample_value_noise():
vec2 fractional_coordinates = sample_coordinates - lattice_coordinates;
vec2 uLattice = lattice01(lattice_coordinates.x, frequency.x);
vec2 vLattice = lattice01(lattice_coordinates.y, frequency.y);
Hasher h = hasher(hash_seed + uint(octave));
Hasher h0 = hasher(h, uLattice.x);
Hasher h1 = hasher(h, uLattice.y);
Hasher h00 = hasher(h0, vLattice.x);
Hasher h10 = hasher(h1, vLattice.x);
Hasher h01 = hasher(h0, vLattice.y);
Hasher h11 = hasher(h1, vLattice.y);
Iterative Refinement of the Lattice
To achieve the seamless effect, we must modify lattice01() in stages:
Step 1: Shifting Negative Coordinates
float l0 = lattice_coordinate;
float l0 = lattice_coordinate;
if (l0 < 0.0) { l0 += frequency; }
float l1 = l0 + 1.0;
Result: Partial tiling on the negative side, but a seam remains.
Step 2: Tiling the Right Boundary
float l1 = l0 + 1.0;
if (l1 >= frequency) { l1 -= frequency; }
Result: Negative side is seamless, but the positive side becomes distorted.
Step 3: Final Tiling Logic
float l0 = lattice_coordinate;
if (l0 < 0.0) {
l0 += frequency;
} else if (l0 >= frequency) {
l0 -= frequency;
}
float l1 = l0 + 1.0;
if (l1 >= frequency) { l1 -= frequency; }
Final Logic Flow
🏁 Conclusion
The resulting pattern tiles perfectly across the region. While this method fails if the range is expanded to , it is perfectly sufficient for our goal of matching UV coordinates and . It remains stable as long as we stay within one unit of the range (i.e., to ).