www.xbdev.net
xbdev - software development
Wednesday January 15, 2025
Home | Contact | Support | WebGPU Graphics and Compute ...
     
 

WebGPU/WGSL Tutorials and Articles

Graphics and Compute ...

 


Cellular Noise - Step by Step



Go step by step using simple visual sketches to explain how simple cellular noise concepts work (like Voronoi noise). Once you understand the basics, and see how you can implement it on a shader - you'll also be able to use the concept to make other patterns/effects.




Start with a standard full screen quad - with the uv coordinates going from 0,0 in the bottom left to 1,1 in the top right.


The basic texture coordinates with 0,0 in the bottom left and 1,1 in the top right.
The basic texture coordinates with 0,0 in the bottom left and 1,1 in the top right.


All of the implementation for the cell noise generator will happen on the fragment shader. We'll use the uv coordinates to generate the noise.

The first thing we're going to do, is scale the uv coordinates - so instead of 0 to 1, it'll go from 0 to 4.

// scale the uv coordinates
uv *= 4.0;



Scale the texture coordinates (multiply them by 4.0) - so intead of 0.0 to 1.0 they go from 0.0 to 4.0.
Scale the texture coordinates (multiply them by 4.0) - so intead of 0.0 to 1.0 they go from 0.0 to 4.0.



Scaling the uv coordinates is as easy as just multiplying it by 4.0. Later on you can scale up the cell noise more so it's more dense - at the moment, for the tutorial steps, we'll start with 4 - so we can look at things closely.

If you want to visualize your UV coordinates - and see them before and after the scaling - you can simply pass them out from the fragment shader as the red and green values. You'll see a nice gradient color pattern.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{
    var 
uv fragCoord// 0-1 full screen
    
    
uv *= 4.0;
    
    return 
vec4<f32>( uv0.01.0);
}




Visualize the modified texture coorinates (using color).
Visualize the modified texture coorinates (using color).


Next we're going to convert the uv coordinates into two parts the whole number and the fractal part of the number.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{
    var 
uv fragCoord// 0-1 full screen
    
    
uv *= 4.0;
    
    var 
flooruv 4.0 ); // uv - 0,   1,   2,   3, 
    
var fractuv 4.0 ); // uv - 0-1, 0-1, 0-1, 0-1
    
    
    
return vec4<f32>( uv0.01.0);
}



Integers and fractions - break the texture coordinates into whole number values and the fractional part.
Integers and fractions - break the texture coordinates into whole number values and the fractional part.


Draw a small dot at the centre of each cell! We'll also draw some lines on the boundaries to show the rectangle regions.



Dots and boxes - divide the space into regions (using the whole numbers and fractions). Visualize this by drawing lines to show...
Dots and boxes - divide the space into regions (using the whole numbers and fractions). Visualize this by drawing lines to show each 'region' and its 'centre'.


The following shader code draws horizontal and vertical lines for the rectangle regions - and a little red dot for the centre.
@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{
    var 
uv fragCoord// 0-1 full screen
    
    
uv *= 4.0;
    
    var 
flooruv ); // uv - 0,   1,   2,   3, 
    
var fractuv ); // uv - 0-1, 0-1, 0-1, 0-1
    
    
let cellSize vec2(1.0);
    
   
    var 
color vec3(1.0); // background white
   
    // red dot in middle of each square
    
let centreDot cellSize*0.5;
    
let sizeOfDot 0.04
    if ( 
lengthuv centreDot ) < sizeOfDot )
    {
        
color vec3(1.00.00.0); // red
    
}
    
    
// draw edges for each square
    
let lineThickness 0.01;
    if (
abs(f.x) < lineThickness || abs(f.1.0) < lineThickness || 
        
abs(f.y) < lineThickness || abs(f.1.0) < lineThickness
    {
        
color vec3(0.0);
    }

    return 
vec4<f32>( color1.0 );
}



Draw the dot for each cell (square) and the lines showing the cell regions.
Draw the dot for each cell (square) and the lines showing the cell regions.


For each dot, we can add a small random number value (+/- 0.5) to each fractal part - so the dot in each cube is randomly positioned. As we're using the original uv coordinates as the seed for the random number generate - the random number stays the same for each update.

Random number uses the original none scaled uv coordinates as the seed - so it stays the same for each frame update - but is unique for each pixel.


Withe one extra line to the shader we can add randomness to all the dots.
let centreDot cellSize*0.5 + (-0.5 random(i)); // randomness -0.5 to 0.5



All the dots are given a random offset.
All the dots are given a random offset.




Randomizing the dot position.
Randomizing the dot position.



This is the updated shader with the extra
random(..)
added in to offset the point centres.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{
    var 
uv fragCoord// 0-1 full screen
    
    
uv *= 4.0;
    
    var 
flooruv ); // uv - 0,   1,   2,   3, 
    
var fractuv ); // uv - 0-1, 0-1, 0-1, 0-1
    
    
let cellSize vec2(1.0);
    
   
    var 
color vec3(1.0); // background white
   
    // red dot in middle of each square
    
let centreDot cellSize*0.5 + (-0.5 random(i)); // randomness -0.5 to 0.5
    
    
let sizeOfDot 0.04
    if ( 
lengthuv centreDot ) < sizeOfDot )
    {
        
color vec3(1.00.00.0); // red
    
}
    
    
// draw edges for each square
    
let lineThickness 0.01;
    if (
abs(f.x) < lineThickness || abs(f.1.0) < lineThickness || 
        
abs(f.y) < lineThickness || abs(f.1.0) < lineThickness
    {
        
color vec3(0.0);
    }

    return 
vec4<f32>( color1.0 );
}



Output for the shader with the randomness added to offset the dot for each cell (square).
Output for the shader with the randomness added to offset the dot for each cell (square).



At this point we have all the key information - so its just a matter of using it! Each cell can lookup the surrounding cells and get the position of its neighbours dots.

for (var y:i32= -1<= 1y++) {
    for (var 
x:i32= -1<= 1x++) {
        
// Neighbor offset in grid (+/-1)
        
var neighbor vec2(f32(x),f32(y));
        ....




Each dot can look at the neighbouring dot positions.
Each dot can look at the neighbouring dot positions.



What we're going to do - for each uv coordinate - we're going to go through all 9 regions (squares) and check which is the closest. We'll use a random number - seeded using the neighbor index.

Below is the code - in fact we don't need the prvious code for finding the centreDot position - we'll calculate it again when we loop over all the regions. All we want to know is the closest point for the 9 cell regions (squares).

We keep a
minDist
variable outside the loop to keep track of this - the resulting output is a set of regions.


    var minDist 1.0;
    
    for (var 
y:i32= -1<= 1y++) {
    for (var 
x:i32= -1<= 1x++) {
        
// Neighbour offset in grid (+/-1)
        
var neighbori vec2(f32(x),f32(y));

        
let neighborCentreDot neighbori cellSize*0.5 + (-0.5 random(neighbori)); 
        
        
let dist length(uv neighborCentreDot);

        if (
dist minDist) {
            
minDist dist;
            
color vec3<f32>(random(neighborCentreDot), random(neighborCentreDot.yx), random(neighborCentreDot.xy));
        }
    }
// end for x
    
}// end for y


What we end up seeing is cellular composition of the areas - each area shown as a different color.


Pixel pixel on the wall - who
Pixel pixel on the wall - who's the closest region of them all! Draw region using the closest point to the uv coordinate.



After the loop has finished - we have the
minDist
value - which is also useful! In fact, instead of drawing a color for each region - we can draw the distance field.

Modify the code to this (remove inner color if statement)

    var minDist 1.0;

    for (var 
y:i32= -1<= 1y++) {
    for (var 
x:i32= -1<= 1x++) {
        
// Neighbour offset in grid (+/-1)
        
        
var neighbori vec2(f32(x),f32(y));

        
let neighborCentreDot neighbori cellSize*0.5 + (-0.5 random(neighbori)); 
        
        
let dist length(uv neighborCentreDot);

    }
// end for x
    
}// end for y
        
    
color vec3minDist );


This produces a nice gradient pattern (cell noise) as shown below:


Cell noise using the distance to the closest point.
Cell noise using the distance to the closest point.



Movement (Animation)


Just for fun, we can spice things up by mixing in a timer counter - pass this timer value into a smoothed noise function to offset the random value. As the timer increments the noise value gradually changes causing the offset to move around (get an animated effect).



Animating the position of the centre location for each region.
Animating the position of the centre location for each region.



The animation of the output is a nice way to debug any value - to see how the result deviates and changes over time for different random values (or if their are any situations that explode or cause chaotic outputs).

The full code for the animated cellular noise with the animation is given below.

fn hash(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

fn 
randomsmoothst:vec2<f32> ) -> f32 
{
    var 
floorst 4.0 ); // uv - 0,   1,   2,   3, 
    
var fractst 4.0 ); // uv - 0-1, 0-1, 0-1, 0-1

    // Four corners in 2D of a tile
    
var hash(i);
    var 
hash(vec2<f32>(1.00.0));
    var 
hash(vec2<f32>(0.01.0));
    var 
hash(vec2<f32>(1.01.0));

    
// Ease-in followed by an ease-out (tweening) for f
    // f = 0.5 * (1.0 - cos( 3.14 * f ) ); 
    // version without cos/sin
    // f = 3*f*f - 2*f*f*f;
    
    
3*f*2*f*f*f;
    
    
// bilinear interpolation to combine the values sampled from the 
    // four corners (a,b,c,d), resulting in a smoothly interpolated value.
   
    // Interpolate Along One Axis - interpolate between 'a' and 'b' using the fractional coordinate 'f.x', 
    // then interpolate between 'c' and 'd' using the same 'f.x'. 
    // This gives two intermediate values, say `e` and `f`.

    // Interpolate Along the Other Axis - linearly interpolate between 'e' and 'f' 
    // using the fractional coordinate 'f.y'. 
    // Final interpolation gives a moothly interpolated value across the square
    
    
var x1 mixabf.);
    var 
x2 mixcdf.);
    
    var 
y1 mixx1x2f.);
    
    return 
y1;


// This function is the 'base' for all other noise functions that are build on noise - so modify it
// to use different random functions for varying effects/distributions
fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{
    var 
uv fragCoord// 0-1 full screen
    
    
uv *= 4.0;
    
    var 
flooruv ); // uv - 0,   1,   2,   3, 
    
var fractuv ); // uv - 0-1, 0-1, 0-1, 0-1
    
    
let cellSize vec2(1.0);
    
    var 
color vec3(1.0); // background white
    
    
var time mytimer*0.5;
   
    
// red dot in middle of each square
    
var centreDot:vec2<f32>;

    var 
minDist 1.0;

    for (var 
y:i32= -1<= 1y++) {
    for (var 
x:i32= -1<= 1x++) {
        
// Neighbour offset in grid (+/-1)
        
        
var neighbori vec2(f32(x),f32(y));
        
        
// Fixed noise
        //let randomval = (-0.5 + random(neighbori));
        
        // Mix it with time so the cell centres move around over time
        
let randomval vec2<f32>( 
                                -
0.5 randomsmooth(neighbori time),
                                 -
0.5 randomsmooth(neighbori time*0.73232)
                             );

        
let neighborCentreDot neighbori cellSize*0.5 randomval
        
        if ( 
x==&& y==)
        {
            
centreDot neighborCentreDot;
        }
        
        
let dist length(uv neighborCentreDot);

        if (
dist minDist) {
            
minDist dist;
            
color vec3<f32>(random(neighborCentreDot), random(neighborCentreDot.yx), random(neighborCentreDot.xy));
        }
        
    }
// end for x
    
}// end for y
        
    
color vec3minDist );

    
// Draw the dot
    
let sizeOfDot 0.04
    if ( 
lengthuv centreDot ) < sizeOfDot )
    {
        
color vec3(1.00.00.0); // red
    
}
    
    
// draw edges for each square
    
let lineThickness 0.01;
    if (
abs(f.x) < lineThickness || abs(f.1.0) < lineThickness || 
        
abs(f.y) < lineThickness || abs(f.1.0) < lineThickness
    {
        
color vec3(0.8);
    }
    
    return 
vec4<f32>( color1.0 );
}




Resources & Links


• Static version for the cell noise steps (LINK)

• Animated version (mix in smoothed noise) (LINK)

• Simple article on how noise patterns work (LINK)

• Information/code for other procedural textures/noise patterns (LINK)
















WebGPU Development Pixels - coding fragment shaders from post processing to ray tracing! WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Games WGSL 2d 3d interactive web-based fun learning WebGPU Compute WebGPU API - Owners WebGPU Development Cookbook - coding recipes for all your webgpu needs! WebGPU & WGSL Essentials: A Hands-On Approach to Interactive Graphics, Games, 2D Interfaces, 3D Meshes, Animation, Security and Production Kenwright graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu for dummies kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright WebGPU Shader Language Development: Vertex, Fragment, Compute Shaders for Programmers Kenwright wgsl webgpugems shading language cookbook kenwright WGSL Fundamentals book kenwright WebGPU Data Visualization Cookbook kenwright Special Effects Programming with WebGPU kenwright WebGPU Programming Guide: Interactive Graphics and Compute Programming with WebGPU & WGSL kenwright Ray-Tracing with WebGPU kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.