logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·29.10.26
← Back to news

Tiling Noise – Procedural GPU Patterns

Original article
catlikecoding.com|35 points|1 comments|by ibobev|Oct 21, 2026

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 000 to 111.

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:

ParameterX ValueY Value
Bumpiness0.2120.2120.2120.6370.6370.637
Base Frequency666444

🔍 Testing the UV Range

To achieve a seamless wrap, the pattern must tile, meaning it repeats identically for coordinates that fall below 000 or exceed 111.

Initially, UV coordinates are set from −0.5-0.5−0.5 to 0.50.50.5. To test the tiling effect, we will expand this range to go from −1-1−1 to 222. 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); 
} 

UV Range Visualization

Since standard UVs range from 000 to 111, this central tile is surrounded by eight others in a 3×33 \times 33×3 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 444, the standard lattice sequence is 0,1,2,3,40, 1, 2, 3, 40,1,2,3,4. This creates pairs: (0,1),(1,2),(2,3),(3,4)(0,1), (1,2), (2,3), (3,4)(0,1),(1,2),(2,3),(3,4). To tile, the final point must wrap back to the start: 0,1,2,3,00, 1, 2, 3, 00,1,2,3,0. The new pairs become: (0,1),(1,2),(2,3),(3,0)(0,1), (1,2), (2,3), (3,0)(0,1),(1,2),(2,3),(3,0).

Implementing lattice01()

We create a function to handle the first (l0l_0l0​) and second (l1l_1l1​) 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 3×33 \times 33×3 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 3×33 \times 33×3 region. While this method fails if the range is expanded to 5×55 \times 55×5, it is perfectly sufficient for our goal of matching UV coordinates 000 and 111. It remains stable as long as we stay within one unit of the 0–10–10–1 range (i.e., −1-1−1 to 222).