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 ...

 

Noise - More Than Just Random Pixels


Let's start with a simpe noise generator function that creates rough noise - just random numbers. We'll normalize the numbers so they're in the range 0.0 to 1.0.

There are lot of various ways of doing this - worth looking into
chaos theory
if this facinates you!


Simple 2d noise function


A popular noise function is one that mixes trignometric functions with large values - the values are so large and wrap around that they create a chaotic pattern which we can sample from using a seed value (offset).

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


If we write a simple fragment shader for a full screen quad, we can take a look at this noise - color each pixel using the value as an rgb color (grayscale) using the uv coordinates as the seed.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
// generate random number
    
var randomNumber random(fragCoord);

    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber); // Use the random number for RGB values

    // Output final color
    
return vec4<f32>(color1.0);
}


The output isn't very exciting - just lots of randompixels - looks like static noise you'd see on an old tv!


Random pixels with no correlation or pattern.
Random pixels with no correlation or pattern.


In fact, you can add an 'offset' to the uv coordinates so it contantly changes (e.g., create a uniform timer called `mytimer`) which we increment each frame.

var randomNumber random(fragCoord mytimer);


This will cause the pixels to constantly change - it doesn't look very nice for pure random pixels as we've got now - but later on as our noise becomes smoother it'll look nicer.

Grayscale to Color


We're also using the same random number for the rgb values - this gives us a grayscale noise - but if we use an offset for each of the seed values we can create color noise.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
// generate random number - one for each color component
    
var randomNumberR random(fragCoord );
    var 
randomNumberG random(fragCoord  vec2(0.233902,1.390239023)   );
    var 
randomNumberB random(fragCoord  vec2(0.93903230.239023902) );

    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumberRrandomNumberGrandomNumberB);

    
// Output final color
    
return vec4<f32>(color1.0);
}


The offset values are chosen arbitrarily just to make each of the seeds unique.


Random color pixels - moving beyond grayscale noise.
Random color pixels - moving beyond grayscale noise.



Smooth out the Noise


Taking things a bit further - we can make the noise a little less harsh - smooth it out a bit by interpolating between values to create a more organic look.

The noise is still the same - however, the noise values are spread out over multiple pixels (interpolate their values).

We'll add an extra function called
randomsmooth(..)
which will use the original
random(..)
function - but smooth the result out.


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 random(i);
    var 
random(vec2<f32>(1.00.0));
    var 
random(vec2<f32>(0.01.0));
    var 
random(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;

     

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    var 
randomNumber randomsmooth(fragCoord );

    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Output final color
    
return vec4<f32>(color1.0);
}


As before we'll plot the pixel values for the smooth noise using the uv coordinates as the seed.



Smoothing noise using the smoothnoise function by interpolating between noise values.
Smoothing noise using the smoothnoise function by interpolating between noise values.



The secret to the smooth noise function is the first two lines:

var floorst 4.0 ); // uv - 0,   1,   2,   3, 
var fractst 4.0 ); // uv - 0-1, 0-1, 0-1, 0-1


These two lines convert the uv coordinates into integers and floating point values. We scale them to get a denser noise pattern - using
uv
without scaling - means the integer would only be
0-1
for the full screen. But multipling it by 4 means the full screen has 4 numbers - 4 random number which we interpolate the values inbetween using the repeating floating point value.


Color Smooth Noise


We can repeat what we did earlier for the color noise - by adding offsets for the rgb value to create color smooth noise

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
// generate random number - one for each color component
    
var randomNumberR randomsmooth(fragCoord );
    var 
randomNumberG randomsmooth(fragCoord  vec2(0.233902,1.390239023)   );
    var 
randomNumberB randomsmooth(fragCoord  vec2(0.93903230.239023902) );

    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumberRrandomNumberGrandomNumberB);

    
// Output final color
    
return vec4<f32>(color1.0);
}



Color smooth noise.
Color smooth noise.



Plotting a Slice of the Noise (1d slice)


While a top down view of the noise looks good - it'd be nice to see a height plot of a slice of the noice - imagine turning the noise on it's side and taking a slice. See how it changes in height (magnitude differences).

We just take a
constant
horizontal texture coordinate value - to get the height change for that slice. We then draw it on top - so we can see the 2d noise in the background - but draw a 1d slice of it on top as a plot.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
// Add 'mytimer' to make the noise constantly change
    
var randomNumber randomsmooth(fragCoord mytimer );

    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Height plot for a single slice of the noise
    
let lineThickness 0.002// thickeness line for the noise
    
let lineHeight    0.5;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x fragCoord.x;
    
let y randomsmoothvec2(xlineSlice) + mytimer )*lineHeight;
    
    if ( 
lengthfragCoord vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    
// Output final color
    
return vec4<f32>(color1.0);
}        



Plotting a slice of the noise on top to see the height variation pattern (as a magnitude).
Plotting a slice of the noise on top to see the height variation pattern (as a magnitude).



Looking at the Noise Steps


If you want, you can add in a little hack to the smooth noise generator function - so the repeating '0..1' value - hard code it so it doesn't change. This lets you see the fixed value numbers from the random noise generator (without being interpolated).

// Comment this line out
// fract( st * 4.0 ); // uv - 0-1, 0-1, 0-1, 0-1
// Change it to use a constant value of '0.2' or some other constant (between 0 to 1)
var vec2(0.2);



Visualizing the smooth noise step values that are intepolated to create the final smooth noise values.
Visualizing the smooth noise step values that are intepolated to create the final smooth noise values.



Fractal Noise (Fractal Brownian Noise or fbn)


We're going to take things further by adding fractal noise to the mix! One particular type of noise called fractal brownian noise lets us build on what we've done up to now - we can combine multiple layers of noise to create different noise patterns (organoic noise patterns that we'd see in nature).

The Fractal Brownian Noise (FBN) function (called fbn) is given below - which will generating noise using multiple octaves and combining them to produce a beautiful fractal-like result.

fn fbn(stvec2<f32>, octavesu32lacunarityf32gainf32) -> f32 {
    var 
amplitudef32 1.0;
    var 
frequencyf32 1.0;
    var 
valuef32 0.0;

    for (var 
iu32 0uoctaves1u) {
        
value value amplitude smoothnoise(st frequency);
        
frequency frequency lacunarity;
        
amplitude amplitude gain;
    }

    return 
value;
}


Suddenly our simple noise function has all these extra input parameters!

st
: The 2D coordinate for sampling the noise (i.e., seed value)
octaves
: Number of layers of noise to combine.
lacunarity
: How the frequency changes between octaves (e.g., doubling frequency).
gain
: How the amplitude changes between octaves (e.g., halving amplitude).

We can adjust these parameters to create all sorts of different fractal noise patterns! Exciting?


Lets modify the fragment shader to plot the output using our new fbn function. We need to add in a few
clamp
values - as the fbn noise function can create output greater and less than 1.0 for our input paramters. So the output stays within 0 to 1 we just clamp the values.

This is the fragment shader using the fbn noise function - the default values are just 1 for the inputs. It produces an output noise the same as the base noise (i.e., smooth noise pattern) - as shown below.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    1;
    
let lacunarity:f32 1.0;
    
let gain:f32       1.0;
    var 
randomNumber fbn(fragCoord mytimeroctaveslacunaritygain );
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Height plot for a single slice of the noise
    
let lineThickness 0.002// thickeness line for the noise
    
let lineHeight    0.8;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x fragCoord.x;
    var 
fbnvec2(xlineSlice) + mytimeroctaveslacunaritygain  );
    
clampy0.01.0 ) * lineHeight;
    
    if ( 
lengthfragCoord vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    
// Output final color
    
return vec4<f32>(color1.0);
}




Fractal brownian noise function - with default simple values (noise values octave:1, lacunarity:1, gain:1).
Fractal brownian noise function - with default simple values (noise values octave:1, lacunarity:1, gain:1).



Tinkering with FBN Parameters



Make the octave value 2 - we can see it will mangnify the output causing it to saturate and clip against the 1.0 ceiling.

    let octaves:u32    2;
    
let lacunarity:f32 1.0;
    
let gain:f32       1.0;




Increasing the octave value to 2.
Increasing the octave value to 2.



Of course, we can scale the gain parameter down to 0.1 to counter act the saturation effect:

    let octaves:u32    2;
    
let lacunarity:f32 1.0;
    
let gain:f32       0.1;



Octabe of 2 and a gain of 0.1.
Octabe of 2 and a gain of 0.1.



As you can see, you can tinker with the parmeters to create various noise patterns - sort of like adding lots of control nobs to your noise!


For example modifying the octabe, lacunarity and gain to the following:

    let octaves:u32    12;
    
let lacunarity:f32 6.0;
    
let gain:f32       0.1;


Gives the noise pattern:


Fbn parameters for octabes:12, lacunarity:6, and gain 0.1.
Fbn parameters for octabes:12, lacunarity:6, and gain 0.1.



Rotation


An important component of noise that is not present up to now, is the spinning or swirling you'd see in organic noise patterns. You often see it in smoke or clouds - they don't move in straight lines - they curl and twist.

So we'll mix in a bit of rotation to our noise functions!

For example, we can create a simple 2d rotation matrix
let m mat2x2<f32>(0.800.60, -0.600.80);


Rotate the 2d seed value using multiplication:
let stnew st m;


The new improved fbn function with spin - let's call it
fbnspin(..)
.

fn fbnspin(
    
stinvec2<f32>, 
    
octavesu32
    
lacunarityf32
    
gainf32
    
offsetf32,         // Offset to adjust noise domain
    
distortionvec2<f32// Distortion applied to the position
) -> f32 {
    var 
amplitudef32 1.0;
    var 
frequencyf32 1.0;
    var 
valuef32 0.0;
    var 
stvec2<f32> = stin;

    for (var 
iu32 0uoctaves1u) {
        
// Apply distortion and offset to the position
        
st st distortion sin(st offset);

        
// Sample the noise with the modified position
        
value value amplitude randomsmooth(st frequency);

        
// Update frequency and amplitude for the next octave
        
frequency frequency lacunarity;
        
amplitude amplitude gain;
    }

    return 
value;
}

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    5;
    
let lacunarity:f32 2.0;
    
let gain:f32       0.01;
    
let offset:f32     1.5;
    
let distortion:vec2<f32> = vec2(0.30.4);
    
    var 
randomNumber fbnspin(fragCoord mytimeroctaveslacunaritygainoffsetdistortion );
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Height plot for a single slice of the noise
    
let lineThickness 0.002// thickeness line for the noise
    
let lineHeight    0.8;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x fragCoord.x;
    var 
fbnspinvec2(xlineSlice) + mytimeroctaveslacunaritygainoffsetdistortion   );
    
clampy0.01.0 ) * lineHeight;
    
    if ( 
lengthfragCoord vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    
// Output final color
    
return vec4<f32>(color1.0);
}


Example output for the fbnspin function using the default values above in the code.



Spin componet to the fbn noise function.
Spin componet to the fbn noise function.



Random Marble Function


Write a marble function to create those swirly patterns you see in marble stone!

fn warpedMarbleNoise(
    
positionvec2<f32>, 
    
octavesu32
    
lacunarityf32
    
gainf32
    
offsetf32
    
distortionvec2<f32>, 
    
scalef32,         // Controls the primary sine frequency
    
intensityf32,     // Controls vein contrast
    
warpIntensityf32// Strength of the domain warping
    
warpScalef32      // Scale of the warp noise
) -> f32 {
    
// Step 1: Generate warp distortion using noise
    
let warpOffset vec2<f32>(
        
fbnspin(position warpScaleoctaveslacunaritygainoffsetdistortion),
        
fbnspin(position.yx warpScaleoctaveslacunaritygainoffsetdistortion)
    );

    
// Step 2: Apply warp to position
    
let warpedPosition position warpIntensity warpOffset;

    
// Step 3: Generate base fractal noise
    
let baseNoise fbnspin(warpedPositionoctaveslacunaritygainoffsetdistortion);

    
// Step 4: Apply sine wave for marble pattern
    
let marbleValue sin(warpedPosition.scale intensity baseNoise);

    
// Normalize result to [0, 1] range
    
return 0.5 0.5 marbleValue;
}


This is the updated fragment shader with the default parameters for the marble effect.


@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    5;
    
let lacunarity:f32 2.0;
    
let gain:f32       0.5;
    
let offset:f32     1.2;
    
let distortion:vec2<f32> = vec2(0.10.1);
    
let scale 3.0;
    
let intensity 5.0;
    
let warpIntensity 2.0;  // Warp intensity (strength of swirls)
    
let warpScale 0.5;                // Warp scale (frequency of the warp noise)

    
var randomNumber warpedMarbleNoise(fragCoord mytimeroctaveslacunaritygainoffsetdistortionscaleintensitywarpIntensitywarpScale );
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Height plot for a single slice of the noise
    
let lineThickness 0.05// thickeness line for the noise
    
let lineHeight    0.8;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x fragCoord.x;
    var 
warpedMarbleNoisevec2(xlineSlice) + mytimeroctaveslacunaritygainoffsetdistortionscaleintensitywarpIntensitywarpScale   );
    
clampy0.01.0 ) * lineHeight;
    
    if ( 
lengthfragCoord vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    
// Output final color
    
return vec4<f32>(color1.0);
}




Combining fbn noise with a circular spin to amplify the pattern to produce a marble-like effect.
Combining fbn noise with a circular spin to amplify the pattern to produce a marble-like effect.



Simplify Marble Noise


Simplify the marble noise function to use the base
fbn(..)
function (instead of the swirl version) - and still produces good results.

fn warpedMarbleNoise(
    
positionvec2<f32>, 
    
octavesu32
    
lacunarityf32
    
gainf32
    
scalef32,         // Controls the primary sine frequency
    
intensityf32,     // Controls vein contrast
    
warpIntensityf32// Strength of the domain warping
    
warpScalef32      // Scale of the warp noise
) -> f32 {
    
// Step 1: Generate warp distortion using noise
    
let warpOffset vec2<f32>(
        
fbn(position warpScaleoctaveslacunaritygain),
        
fbn(position.yx warpScaleoctaveslacunaritygain)
    );

    
// Step 2: Apply warp to position
    
let warpedPosition position warpIntensity warpOffset;

    
// Step 3: Generate base fractal noise
    
let baseNoise fbn(warpedPositionoctaveslacunaritygain);

    
// Step 4: Apply sine wave for marble pattern
    
let marbleValue sin(warpedPosition.scale intensity baseNoise);

    
// Normalize result to [0, 1] range
    
return 0.5 0.5 marbleValue;
}


The following gives the fractal with the default parameters:

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    8;
    
let lacunarity:f32 3.0;
    
let gain:f32       0.1;
    
let scale          2.0;
    
let intensity 1.0;
    
let warpIntensity 2.0;    // Warp intensity (strength of swirls)
    
let warpScale 1.0;       // Warp scale (frequency of the warp noise)

    
var randomNumber warpedMarbleNoise(fragCoordoctaveslacunaritygainscaleintensitywarpIntensitywarpScale );
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);

    
// Height plot for a single slice of the noise
    
let lineThickness 0.05// thickeness line for the noise
    
let lineHeight    0.8;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x fragCoord.x;
    var 
warpedMarbleNoisevec2(xlineSlice), octaveslacunaritygainscaleintensitywarpIntensitywarpScale   );
    
clampy0.01.0 ) * lineHeight;
    
    if ( 
lengthfragCoord vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    
// Output final color
    
return vec4<f32>(color1.0);
}



Simpler marble function with only an fbn (not fbnswirl).
Simpler marble function with only an fbn (not fbnswirl).



Grid of Parameters


As you can see, there is lots of paramters for controlling the noise - a nice way to see how the varying parameters compare and procude different effects is to create a grid. Vary one parameter along the x-axis and vary another parameter along the y-axis so you can see how the output changes.

We create a
submain(..)
function - which we call from
main(..)
in the fragment shader - the
submain
is for the cells - with repeating uv coordinates that go from 0 to 1 for each cell. Draw the same noise pattern agan and again but at differnet loacatios with different parameters.

// Submain function (subgrid with its own 0-1 uv range, called subst)
fn submain(stvec2<f32>, substvec2<f32>, x:u32y:u32num:u32) -> vec3<f32>
{
    
// different parameters for each 'cell'
    
let ix f32(x)/f32(num);
    
let iy f32(y)/f32(num);
    
    
let octaves:u32    8;
    
let lacunarity:f32 3.0;
    
let gain:f32       0.1;
    
let scale          2.0;
    
let intensity      1.0;
    
let warpIntensity  0.0 2.0*ix;    // Warp intensity (strength of swirls)
    
let warpScale      0.0 2.0*iy;    // Warp scale (frequency of the warp noise)

    
var randomNumber warpedMarbleNoise(substoctaveslacunaritygainscaleintensitywarpIntensitywarpScale );
    
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);
    
    return 
color;
}

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let gridSize 5.0;
    
let cellSize 1.0 gridSize;

    
// Determine which grid cell the fragment belongs to
    
let gridX:f32 floor(fragCoord.cellSize);
    
let gridY:f32 floor(fragCoord.cellSize);
    
let cellCoord vec2<f32>(gridXgridY);

    
// Normalize fragment coordinates within the cell (subst)
    
let subst = (fragCoord cellSize) / cellSize;

    
// Call the submain function for the current cell
    
let color submain(cellCoordsubstu32(gridX), u32(gridY), u32(gridSize) );
    
    return 
vec4(color1.0);
}


The output looks like this for a 5x5 grid of noise patterns.


Comparison of varying the warpIntensity and warpScale values from 0 to 2.0 in a 5x5 grid (top left is 0,0) and the bottom right...
Comparison of varying the warpIntensity and warpScale values from 0 to 2.0 in a 5x5 grid (top left is 0,0) and the bottom right is (2,2).



Vary lacunarity agains warpScale


Create another grid but using the
lacunarity
and
warpScale
.

These are the default parameters and value (lacunarity goes from 0 to 32 and warpScale from 0 to 2).

    let octaves:u32    8;
    
let lacunarity:f32 32.0*ix;
    
let gain:f32       0.1;
    
let scale          2.0;
    
let intensity      1.0;
    
let warpIntensity  1.0;             // Warp intensity (strength of swirls)
    
let warpScale      0.0 2.0*iy;    // Warp scale (frequency of the warp noise)



Comparison of varying lacunarity against warpScale.
Comparison of varying lacunarity against warpScale.



Plot Height Slice in SubCells


We can also add in the code from earlier to draw the height information using a slice of the random noise (1d view of the noise isntead of the 2d).

Add in the extra code to the bottom of the
submain(..)
like this:

fn submain(stvec2<f32>, substvec2<f32>, xx:u32yy:u32num:u32) -> vec3<f32>
{
    
// different parameters for each 'cell'
    
let ix f32(xx)/f32(num);
    
let iy f32(yy)/f32(num);
    
    
let octaves:u32    8;
    
let lacunarity:f32 32.0*ix;
    
let gain:f32       0.1;
    
let scale          2.0;
    
let intensity      1.0;
    
let warpIntensity  1.0;             // Warp intensity (strength of swirls)
    
let warpScale      0.0 2.0*iy;    // Warp scale (frequency of the warp noise)

    
var randomNumber warpedMarbleNoise(substoctaveslacunaritygainscaleintensitywarpIntensitywarpScale );
    
    
randomNumber clamp(randomNumber0.01.0);
    
    
// Use random number for fragment color (for demonstration)
    
var colorvec3<f32> = vec3<f32>(randomNumber);
    
    
// Plot the height in the subgrid
    
    // Height plot for a single slice of the noise
    
let lineThickness 0.02// thickeness line for the noise
    
let lineHeight    0.8;   // max line height half screen height
    
let lineSlice     0.2;   // cut across the noise at 0.2 value
    
    
let x subst.x;
    var 
warpedMarbleNoisevec2(xlineSlice), octaveslacunaritygainscaleintensitywarpIntensitywarpScale   );
    
clampy0.01.0 ) * lineHeight;
    
    if ( 
lengthsubst vec2<f32>(x,y) ) < lineThickness )
    {
        
color vec3<f32>(1,0,0);
    }
    
    return 
color;
}




Overlay the height plot for a 1d noise slice.
Overlay the height plot for a 1d noise slice.



Color Marble


We've just been focusing on the noise pattern - with no color! If we want a marble like color pattern - this don't just want random rgb values - instead the marble color varies between two colors - use the random value as the blend factor!


let marbleColor mix(vec3<f32>(0.90.90.9), vec3<f32>(0.30.30.3), randomNumberMarble);



Now we have a color marble effect


Mix in a bit of color with the random marble pattern - so it isn
Mix in a bit of color with the random marble pattern - so it isn't so 'black and white' :)


@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    8;
    
let lacunarity:f32 2.0;
    
let gain:f32       0.1;
    
let scale          2.0;
    
let intensity      1.0;
    
let warpIntensity  1.0;     
    
let warpScale      1.0;   

    var 
randomNumber warpedMarbleNoise(fragCoordoctaveslacunaritygainscaleintensitywarpIntensitywarpScale );
    
    
randomNumber clamp(randomNumber0.01.0);
    
    var 
color mix(vec3<f32>(0.90.90.0), vec3<f32>(0.50.00.5), vec3(randomNumber) );
    
    return 
vec4(color1.0);
}



Color Gradients


Instead of just 2 colors - you can also use color functoins to map the value of 0 to 1 to a rainbow (e.g., blue, green, orange, ...red).


Color gradient function for the random noise values.
Color gradient function for the random noise values.


fn heatGradient(valuef32) -> vec3<f32> {
    
// Clamp the input value to the range [0, 1]
    
let v clamp(value0.01.0);

    
// Define key points in the gradient
    
if (0.25) {
        
// Blue to Green (0.0 - 0.25)
        
let t 0.25;
        return 
mix(vec3<f32>(0.00.01.0), vec3<f32>(0.01.00.0), t);
    } else if (
0.5) {
        
// Green to Yellow (0.25 - 0.5)
        
let t = (0.25) / 0.25;
        return 
mix(vec3<f32>(0.01.00.0), vec3<f32>(1.01.00.0), t);
    } else if (
0.75) {
        
// Yellow to Orange (0.5 - 0.75)
        
let t = (0.5) / 0.25;
        return 
mix(vec3<f32>(1.01.00.0), vec3<f32>(1.00.50.0), t);
    } else {
        
// Orange to Red (0.75 - 1.0)
        
let t = (0.75) / 0.25;
        return 
mix(vec3<f32>(1.00.50.0), vec3<f32>(1.00.00.0), t);
    }
}

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let octaves:u32    4;
    
let lacunarity:f32 1.0;
    
let gain:f32       0.2;
    
let scale          1.7;
    
let intensity      2.0;
    
let warpIntensity  3.0;     
    
let warpScale      1.6;   

    var 
randomNumber warpedMarbleNoise(fragCoordoctaveslacunaritygainscaleintensitywarpIntensitywarpScale );
    
    
randomNumber clamp(randomNumber0.01.0);
    
    var 
color heatGradientrandomNumber );
    
    return 
vec4(color1.0);
}


Color gradient functions are very useful in data analysis for identifying problem areas and to quickly see the distribution/gradients. As using a grayscale it's difficult to see the minimum and maximum values of various areas (as it's hard to see if it's very dark gray or black). The color range easily lets us see which areas sit within which ranges (blue as 0 in this case and red as the maximum 1).


Focusing on SWIRLING


Let's take a step back and just look at swirling - and combining it with noise in different ways.


So that we have a point of reference - let's create a pattern function - creates color squares - so we can see how much the swirl is impacting the underlying image (layer we'll swap this out for noise).

fn squarePattern(uvvec2<f32>) -> vec3<f32> {
    
let gridSizef32 10.0;
    
    
// Calculate the grid cell position
    
let cell vec2<u32>( abs(uv) * gridSize);
    
// Create a unique index for the cell
    
let index cell.cell.u32(gridSize); 
    
    return 
vec3randomvec2f32(index  ) ) ),
                 
randomvec2f32(index+1) ) ),
                 
randomvec2f32(index+2) ) ) );
}


The uv coordinates in the squarePattern are 'abs' so that the pattern works outside of the uv range (even for negative values). Without the abs(uv) the negative coordinates don't repeat.

This produces the following tidy pattern (screen of diffent color squares).



Random color squares.
Random color squares.



Simple swirl around the middle of the screen.


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
center  vec2<f32>(0.50.5);
  var 
texSize vec2<f32>(1.0011.001);
  var 
tc uvs texSize;
  
tc tc center;
  
  var 
radius 0.1 mytimer;
  var 
angle 1.2;

  var 
dist length(tc);
  if (
dist radius
  {
    var 
percent = (radius dist) / radius;
    var 
theta percent percent angle 8.0;
    var 
sin(theta);
    var 
cos(theta);
    
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
  }
  
tc tc center;
  
  var 
coord tc texSize;
  var 
color squarePattern(coord );

  return 
vec4<f32>(color.xyz1.0);
}


The swirly pattern is updated using the
mytimer
value which impacts the radius of the swirl - so as the radius grows you can see the swirl increase. As shown below.


Distoring the image pattern in a swirly motion.
Distoring the image pattern in a swirly motion.



Random Radius


Take the previous example but this time, we'll mix in some randomness with the radius distance - instead of it being constant we'll change it using a smooth noise function from earlier.

Step one - create random noise pattern using fixed radius of 0.5.


Swirl pattern with a fixed radius of 0.5.
Swirl pattern with a fixed radius of 0.5.


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
center  vec2<f32>(0.50.5);
  var 
texSize vec2<f32>(1.0011.001);
  var 
tc uvs texSize;
  
tc tc center;
  
  var 
radius 0.5;
  var 
angle 1.2;

  var 
dist length(tc);
  if (
dist radius
  {
    var 
percent = (radius dist) / radius;
    var 
theta percent percent angle 8.0;
    var 
sin(theta);
    var 
cos(theta);
    
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
  }
  
tc tc center;
  
  var 
coord tc texSize;
  var 
color squarePattern(coord );

  return 
vec4<f32>(color.xyz1.0);
}



Step Two - multiply the fixed radius by the smoothed noise function.


Smoothed noise for the radius distance instead of a fixed.
Smoothed noise for the radius distance instead of a fixed.


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
center  vec2<f32>(0.50.5);
  var 
texSize vec2<f32>(1.0011.001);
  var 
tc uvs texSize;
  
tc tc center;
  
  var 
radius 0.5 randomsmooth(2.0 uvs);
  var 
angle 1.2;

  var 
dist length(tc);
  if (
dist radius
  {
    var 
percent = (radius dist) / radius;
    var 
theta percent percent angle 8.0;
    var 
sin(theta);
    var 
cos(theta);
    
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
  }
  
tc tc center;
  
  var 
coord tc texSize;
  var 
color squarePattern(coord );

  return 
vec4<f32>(color.xyz1.0);
}


Random Position for Swirl


Instead of randomizing the radius - we can keep the
dist
variable fixed and modify the centre location (currently
(0.5,0.5)
). Use the same
randomsmooth(..)
function so the center is randomized and see the pattern we get.



Randomize the position using a smoothnoise function.
Randomize the position using a smoothnoise function.


@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  var 
center  vec2<f32>(0.50.5);
    
  
center vec2<f32>( randomsmooth(1.0 uvs),
                      
randomsmooth(1.0 * (uvs vec2(0.328903232)) ) );
    
  var 
texSize vec2<f32>(1.001 );
  var 
tc uvs texSize;
  
tc tc center;
  
  var 
radius 1.0;
  var 
angle 0.2;

  var 
dist length(tc);
  if (
dist radius
  {
    var 
percent = (radius dist) / radius;
    var 
theta percent percent angle 8.0;
    var 
sin(theta);
    var 
cos(theta);
    
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
  }
  
tc tc center;
  
  var 
coord tc texSize;
  var 
color squarePattern(coord );

  return 
vec4<f32>(color.xyz1.0);
}


Create a Table Comparing Parameters (Swirl)



As we did earlier, let's create a 3x3 grid of outputs - for each output change the parameter. We'll modify one parameter (the angle) - changing it by both amplitude and frequency. Horizontal will be the frequency while the vertical will be the amplitude.


Randomizing the swirl angle - comparing the deviation in frequency and amplitude.
Randomizing the swirl angle - comparing the deviation in frequency and amplitude.



// Submain function (subgrid with its own 0-1 uv range, called subst)
fn submain(stvec2<f32>, substvec2<f32>, xx:u32yy:u32num:u32) -> vec3<f32>
{
    
let dx f32(xx)/f32(num+1);
    
let dy f32(yy)/f32(num+1);
        
    var 
center  vec2<f32>(0.50.5);

    var 
texSize vec2<f32>(1.001 );
    var 
tc subst texSize;
    
tc tc center;

    var 
radius 1.0;
    var 
angle  0.1 dy*0.5 randomsmoothdx 12.0 subst);

    var 
dist length(tc);
    if (
dist radius
    {
      var 
percent = (radius dist) / radius;
      var 
theta percent percent angle 8.0;
      var 
sin(theta);
      var 
cos(theta);
      
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
    }
    
tc tc center;

    var 
coord tc texSize;
    var 
color squarePatterncoord );

    return 
color.xyz;
}


@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let gridSize 3.0;
    
let cellSize 1.0 gridSize;

    
// Determine which grid cell the fragment belongs to
    
let gridX:f32 floor(fragCoord.cellSize);
    
let gridY:f32 floor(fragCoord.cellSize);
    
let cellCoord vec2<f32>(gridXgridY);

    
// Normalize fragment coordinates within the cell (subst)
    
let subst = (fragCoord cellSize) / cellSize;

    
// Call the submain function for the current cell
    
let color submain(cellCoordsubstu32(gridX), u32(gridY), u32(gridSize) );
    
    return 
vec4(color1.0);
}


Levels or Rotation


The fbn function as inspiration - which combines multiple layers of noise - but each layer has a different scale/frequency - we can do a simple version of this using matrix rotations.


Combining multiple rotations to add detail to the twisty noise effect.
Combining multiple rotations to add detail to the twisty noise effect.


The different levels of rotation are scaled so that we can add levels of detail to the twistly swirly pattern. With the larger amplitudes creating the larger swirls, while smaller amplitudes with higher frequency create the smaller swirls on top.

@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let dt mytimer 1.0;
    
    
let ang1 0.4 randomsmooth2.5 fragCoord dt );
    
let m1 mat2x2<f32>(cos(ang1), -sin(ang1), sin(ang1), cos(ang1));
    
    
let ang2 0.1 randomsmooth6.0 fragCoord dt );
    
let m2 mat2x2<f32>(cos(ang2), -sin(ang2), sin(ang2), cos(ang2));
    
    
let ang3 0.05 randomsmooth12.0 fragCoord dt );
    
let m3 mat2x2<f32>(cos(ang3), -sin(ang3), sin(ang3), cos(ang3));
    
    
let octaves:u32    8;
    
let lacunarity:f32 128.0;
    
let gain:f32       0.01;
    
    var 
fbn(      1.0  fragCoord m1 +
                      
0.6  fragCoord m2 
                      
0.2 fragCoord m3,
                             
octaveslacunaritygain ) * 0.8;
    
     return 
vec4(ccc1.0);
}


Hipnotic Noise Break


Just at this point - let's throw in some random values and make them large! So the pattern noise is hit by all sorts of issues - too much detail in too few pixels - creating a mesmorizong mess that you can't stop looking at!


Going crazy with noise.
Going crazy with noise.


@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    
let dt mytimer 1.0;
    
    
let ang1 10.0 randomsmooth0.1 fragCoord dt );
    
let m1 mat2x2<f32>(cos(ang1), -sin(ang1), sin(ang1), cos(ang1));
    
    
let ang2 10.0 randomsmooth1.0 fragCoord dt );
    
let m2 mat2x2<f32>(cos(ang2), -sin(ang2), sin(ang2), cos(ang2));
    
    
let ang3 1.0 randomsmooth( (3.0) * fragCoord dt );
    
let m3 mat2x2<f32>(cos(ang3), -sin(ang3), sin(ang3), cos(ang3));
    
    
let octaves:u32    8;
    
let lacunarity:f32 128.0;
    
let gain:f32       0.01;
    
    var 
fbn(   10.0*   1.0  fragCoord m1 +
                   
10.0*   1.0  fragCoord m2 
                   
35.0*   1.0 fragCoord m3,
                             
octaveslacunaritygain ) * 0.8;
    
     return 
vec4(ccc1.0);
}


The noise to me feels painful to watch - it has a rythmic sub-pattern that you can make out - but it feels uncomofortable - like scratching your finger nails on a chalk board.


More Crazy Noise Patterns


Continuing with the experimental noise patterns - we can also mix the smooth noise with the square texture pattern we created for testing.

The uv coordinate are taken from the smooth random noise function - we also add a timer offset so it can be animated! Creating a hypnotic pattern seems to never end.


Random texture coordinates for the grid texture pattern.
Random texture coordinates for the grid texture pattern.


@fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    var 
st vec2randomsmoothvec2(2.0 fragCoord ) ) ) + mytimer;
    
    var 
color squarePatternst );
    
    return 
vec4(color1.0);
}


Beyond Standard Noise (Voronoi)


At this point, we've just been using basic smoothed noise in various ways (e.g., scaling it, twisting it, stacking it and so on). Now it's time to look at a few other noise functions.


Voronoinoise function output.
Voronoinoise function output.


We define a
voronoinoise(..)
function - which works similar to the previous noise functions - pass in a 2d seed and it generates a random number.


fn voronoinoisepoint:vec2<f32> ) -> f32
{
    var 
p:vec2<f32> = floorpoint 4.0 );
    var 
f:vec2<f32> = fractpoint 4.0 );
    var 
res 0.0;
    for( var 
j:i32 = -1j<=1j++ ) {
        for( var 
i:i32 = -1i<=1i++ ) {
            var 
vec2<f32>( f32(i), f32(j) );
            var 
vec2<f32>( ) - randomb);
            
res += 1./pow(dot(r,r),8.);
        }
    }
    return 
pow(1./res0.0625);
}

@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    var 
randomNumber voronoinoisefragCoord );
    
    var 
color vec3<f32>( randomNumber );
    
    return 
vec4(color1.0);
}



Worley Noise


Worley Noise, also known as Cellular Noise, generates a pattern based on the distance to the nearest feature points, creating organic, cell-like structures.

Compared to the previous noise pattern - Voronoi Noise, on the other hand, divides space into regions based on the closest distance to a set of seed points, producing a tessellated pattern similar to a mosaic.



Worley noise generator output.
Worley noise generator output.


fn worleyNoise(coordvec2<f32>) -> f32 {
    var 
minDistf32 1e9;
    var 
featurePointvec2<f32>;
    
    
// Generate feature points and calculate distance
    
for (var ii32 = -1<= 11) {
        for (var 
ji32 = -1<= 11) {
            
featurePoint floor(coord) + vec2<f32>(f32(i), f32(j)) + fract(sin(vec2<f32>(dot(floor(coord) + vec2<f32>(f32(i), f32(j)), vec2<f32>(12.989878.233))) * 43758.5453));
            var 
distf32 length(coord featurePoint);
            if (
dist minDist) {
                
minDist dist;
            }
        }
    }
    return 
minDist;
}


@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    var 
randomNumber worleyNoisefragCoord 4.0 );
    
    var 
color vec3<f32>( randomNumber );
    
    return 
vec4(color1.0);
}



Gabor Noise


Gabor noise is a bit different as it's a type of procedural texture. It gets the name, as it uses Gabor filters, which are wavelets with a Gaussian envelope, to create smooth and continuous noise patterns.

The output for Gabor noise is a highly detailed, anisotropic textures that can simulate natural phenomena like wood grain or turbulence.


Gabar noise pattern for a frequency of 10 and a sigma of 0.5.
Gabar noise pattern for a frequency of 10 and a sigma of 0.5.


fn gaborNoise(coordvec2<f32>, frequencyf32sigmaf32) -> f32 {
    var 
noisef32 0.0;
    
let PIf32 3.141592653589793;
    
    for (var 
ii32 = -1<= 11) {
        for (var 
ji32 = -1<= 11) {
            
// Create a random direction and phase
            
var thetaf32 2.0 PI fract(sin(dot(floor(coord) + vec2<f32>(f32(i), f32(j)), vec2<f32>(12.989878.233))) * 43758.5453);
            var 
phasef32 2.0 PI fract(sin(dot(floor(coord) + vec2<f32>(f32(i) + 0.5f32(j) + 0.5), vec2<f32>(12.989878.233))) * 43758.5453);
            var 
offsetvec2<f32> = vec2<f32>(cos(theta), sin(theta));

            
// Calculate the Gabor kernel
            
var xvec2<f32> = coord - (floor(coord) + vec2<f32>(f32(i), f32(j)));
            var 
rf32 length(x);
            var 
gaborf32 exp(-/ (2.0 sigma sigma)) * cos(2.0 PI frequency dot(offsetx) + phase);

            
// Accumulate the noise value
            
noise noise gabor;
        }
    }
    return 
noise;
}


@
fragment
fn main(@location(0fragCoord vec2<f32>) -> @location(0vec4<f32
{   
    var 
randomNumber gaborNoisefragCoord 10.00.5 );
    
    var 
color vec3<f32>( randomNumber );
    
    return 
vec4(color1.0);
}



Metalic Noise


The noise pattern gives the feeling that the surface is metalic and reflective.


Metalic noise pattern.
Metalic noise pattern.


// Metalic noise swirly...
@fragment
fn main(@location(0uvs    vec2<f32>) -> @location(0vec4<f32
{
  
let nn 1.0/( 0.5 + (randomsmoothuvs mytimer*4.0 ))*0.5 );
    
  var 
center  vec2<f32>(0.5*nn0.5*nn);
  var 
texSize vec2<f32>(1.0011.001);
  var 
tc = ((uvs*(1.0/nn)) % (1.0*nn)  ) * texSize;
  
tc = (tc center);
  
  var 
radius 0.5 randomsmoothuvs mytimer*4.0 );
  var 
angle 1.2;

  var 
dist length(tc);
  if (
dist radius
  {
    var 
percent = (radius dist) / radius;
    var 
theta percent percent angle 8.0;
    var 
sin(theta);
    var 
cos(theta);
    
tc vec2<f32>(dot(tcvec2<f32>(c, -s)), dot(tcvec2<f32>(sc)));
  }
  
tc tc center;
  
  var 
coord tc texSize;
  
//var color = squarePattern(coord );
    
  
var color vec3randomsmoothcoord*4.0 ) );

  return 
vec4<f32>(color.xyz1.0);
}






Resources & Links


• Example Code Samples - 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.